Chapter 4. Step-by-Step Migration Guide with Atomic Design
This chapter provides step-by-step instructions for doing an asynchronous rewrite with atomic design. It’s a reference to use while undertaking a migration, so some parts might be relevant right now and others can be bookmarked for later. In this chapter, we’ll take you through the example of migrating a frontend application, although these techniques could also be applied to mobile or backend applications.
“See This Step on GitHub” Sections
If you end up getting stuck or just want to follow along without writing code you can clone the Git repositories and check out the examples at various points. Just look for the “See This Step on GitHub” sidebar after a set of instructions. All of the checkpoints for the examples are tagged in releases:
https://github.com/hharnisc/my-monorepo-boilerplate/releases
https://github.com/hharnisc/my-component-library-boilerplate/releases
Getting Set Up
Here’s a quick reference of some of the technology and tooling that we’ll be using to undertake this migration.
- Git
- The version control system we’ve used. Other version control systems could be used as well.
- React
- A JavaScript framework for building user interfaces.
- Yarn
- A dependency manager for JavaScript.
- Yarn Workspaces
- A simpler way to manage multiple JavaScript packages within one repository.
- Webpack
- An open source JavaScript module bundler.
- Storybook
- An interactive development and testing environment for UI components.
- Jest
- A unit testing framework for JavaScript.
Evaluating and Adjusting the Current Development Environment
To find the starting point (and amount of extra foundational work) that’s going to be most beneficial for you, evaluate the state of application development within the current product.
A great starting state for your existing application is one that has these two main qualities:
- Some kind of package dependency management system, ideally with NPM or Yarn
- A build system like webpack to bundle your dependencies
Next, we’ll outline four common stages of development environment, with suggestions for changes you can make to set up the environment well for a smooth migration.
Stage 1: Manual dependency management
For the purpose of finding your starting point, we’ll call manual dependency management Stage 1.
There are a couple of common ways to manually manage dependencies (Examples 4-1 and 4-2).
Example 4-1. A script fetched from a CDN
<script
src=
'https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js'
/>
Example 4-2. A script fetched from a local static file
<head>
<script
src=
'//static/scripts/react.js'
/>
</head>
If you’re recognizing either of these in your application, your starting point is manual dependency management. It’ll be difficult to undertake a migration with this setup, so we recommend adding at least a build system to bundle your dependencies, and loading that one bundled script (rather than importing each dependency by hand).
Stage 2: Global variables interacting with libraries
Before tools like ES modules and asynchronous module definitions like require.js existed to help manage dependencies, a number of hand-rolled solutions were used to write modular code. Almost all of them add a variable to the window object and use the variable to load dependencies from another file, as in Examples 4-3 and 4-4.
Example 4-3. Adding a library
// myCoolLib.js
var
MyApplication
=
window
.
MyApplication
||
{};
// need to ensure this exists!
MyApplication
.
myCoolLib
=
{
doTheThing
:
function
()
{},
};
Example 4-4. Loading the library
// Application.js
var
myCoolLib
=
window
.
MyApplication
.
myCoolLib
;
myCoolLib
.
doTheThing
();
This gets the job done, but leads to trouble as the codebase grows. Ensuring the MyApplication
variable exists and doesn’t replace the existing one might cause duplicated code (although duplication can be solved with libraries).
A bigger danger is that it’s possible to accidentally replace the global MyApplication
variable, which will silently break the application without throwing an error.
Even worse than unwittingly breaking everything is that with this set up, dependencies need to be resolved by hand and the library and application loaded in the right order for the application to load at all—loading Application.js
before myCoolLib.js
is loaded won’t work. It needs to remain in order as in Example 4-5.
Example 4-5. Resolving dependencies by hand in the head tag
<head>
<script
src=
"/static/myCoolLib.js"
/>
<script
src=
"/static/Application.js"
/>
</head>
Debugging an application that isn’t loading correctly because a script was placed out of order is tedious because each file will need to be manually inspected.
Since the script order matters, scripts must be loaded synchronously. This delays the first render and makes the page feel slow, another disadvantage.
If this is your starting point, we recommend using ES modules and webpack to solve load order issues. Doing this means there will be fewer files to load, and it’ll be easier to import code from another part of the project.
The new workflows can be done alongside the existing work, as in Example 4-6.
Example 4-6. Adding a bundle to the existing work
<head>
<!-- New UI can be consumed by the existing one -->
<script
src=
"/static/bundle.js"
/>
<script
src=
"/static/myCoolLib.js"
/>
<script
src=
"/static/Application.js"
/>
</head>
Stage 3: 100% server-side rendered
Server-side rendered applications take data from the database or from memory and plug it into templates, render the template, and then send the rendered template to the client.
If this is your setup, we recommend switching to client-side rendering with your server-side rendered templates. Switching to client-side rendering has a few advantages over server-side rendering that will make it helpful in the migration process. Rendering on the client side decouples the release process between the backend and frontend applications. This enables the migration team to operate at its own pace, rather than have a dependency on another product release cycle. When clients are rendering the application, this will reduce the load placed on the servers and reduce server costs. The compute time involved in rendering is dispersed among the client’s browsers. Even further cost savings and boosted performance can come from using a CDN to deliver the JavaScript bundle. This is because the CDN delivers the payload near the user from a cache.
Here are the steps to move from a server-side rendered application to a client-side application:
- Generate a static JavaScript bundle of client-side code.
- Modify the server to serve the static JavaScript bundle.
- Inject a script tag in the head to load the client-side JavaScript bundle.
- (Optional but recommended) Modify the server to proxy to a development server that watches and rebundles client-side code.
This shifts some of the rendering work from the server to the client, so the primary role of the server is to serve the right data to clients. In turn, the clients are mostly an interface to that data.
Stage 4: Dependency management + module bundling + client-side rendering
The ideal starting point for doing an asynchronous rewrite with atomic design is one where dependencies are managed and bundled with tooling and rendered by the client. This setup has a dependency management system, ideally with NPM or Yarn, a build system like webpack to bundle dependencies, and rendering that is happening on the client.
As the project becomes more complex, this tooling will become more important so that developers can focus on solving problems for customers rather than solving problems with the development environment.
We recommend this setup before undertaking a migration, although it’s possible to use one of the previous setup stages taking the suggested precautions.
Changes in Organization Structure
Depending on your starting point, there will be some key changes in the organization structure of the application. Most applications will start off with an integrated approach, which is structured roughly like Figure 4-1.
The advantage of this structure is that it is easier to get started and doesn’t incur much overhead initially. Creating another feature is as simple as writing a new file and importing it into an existing feature.
Over time, the integrated organization pattern starts to show its warts as the lines between features blur. As more features get built and more people move in and out of the project, it gets harder and harder to work with. When the application gets to a certain level of difficulty to work with, rewriting the entire application is often the best way to fix the project.
To shield against blurry features, we’ll migrate to a more modular approach using local packages. In the modular approach, features are organized into packages with clear boundaries, and the overall application structure looks more like Figure 4-2.
While this doesn’t make blurry features go away entirely, it limits the blast area. Instead of needing to rewrite the entire application, a team might need to rewrite one package—or better yet, do a refactor.
Changes in Data Flow
A common trait of integrated organizational pattern is multidirectional data flow. This is especially true with data that is stored in backbone-like model systems and quickly handmade data stores that use plain JavaScript objects. In multidirectional data flow, state can be passed from any point in the system to another point, like Figure 4-3.
This makes for a difficult-to-understand graph that others will need to traverse when needing to make a change to the application.
While the modular approach might help isolate parts of the data flow, it does necessarily simplify multidirectional data flow. We’ll need to employ other techniques that centralize both communication and state. In Figure 4-4, we see how data flows with this centralized hub.
To achieve this, we’ll create a Store package, which will keep all application state and act as a message broker for actions. The Store package acts as a mediator for actions that occur in the application and ensures that data flows in one direction.
Initializing an Atom Library
In this section, we’ll break down each of the steps to initialize an Atom library in order, with all the commands you’ll need to build, test, and publish an Atom so that other applications can consume it.
We recommend migrating an Atom into the existing application early on in the process. This keeps the focus on creating a library of Atoms that are immediately useful to your new application.
At the end of this section you should have enough structure to build and migrate the first Atom.
Set Up the Component Library
In this section, we’ll give step-by-step instructions for creating your component library repository with Yarn packages and React Storybook for faster development of UI components. We’ll walk through setting up a new Git repository, initializing the package on NPM, and setting up React Storybook.
Setting up a new repository and package is desirable over reusing an existing repository because the component library is a shared resource. As more projects and teams utilize the component library, the need for the component library to have its own release cycle increases. Being tied to any one product release cycle will become problematic.
Introducing React Storybook will help developers build individual components in isolation. This makes it easier to iterate quickly and fix bugs because a developer can think about one small slice of the application at a time.
At the end of this section, you’ll have a working development environment and will be ready to build your first atom!
Step 1: A new Git repository
You’ll need a new Git repository to house your component library. Here’s how to do that on GitHub.
Once your new repository is created, clone it locally by running the command in Example 4-7 in the directory that you want your component library to be in.
Example 4-7. Clone the component library locally
git clone git@github.com:<my-account>/<my-component-library>.git
Step 2: Initialize a package
In your new component library repo, you’ll need to create a Yarn package (Example 4-8). You’ll need to have Yarn installed for this.
Example 4-8. Initialize the component library
cd
my-component-library
yarn init
Yarn will take you through the default questions to create the package. Example 4-9 includes our example answers.
Example 4-9. Example answers for setting up a public package
question name(
my-component-library)
: my-component-library question version(
1.0.0)
: 0.0.1 question description: My New Component Library question entry point(
index.js)
: question git repository: https://github.com/<my-account>/<my-component-library> question author: YOUR NAME question license(
MIT)
: MIT question private: no success Saved package.json
In your component repo, you should now see a node_modules folder and a package.json file.
Private npm Packages
Setting question private:
to no
creates a public package, which makes the code easier to share with anyone on npm
. If you work in an environment where private packages are preferred, setting question private:
to yes
will not change any of the workflows described in this report. However, there will be extra considerations around access controls when sharing the package, which is out of the scope of this report.
To make sure your node modules folder isn’t tracked in version control, add a .gitignore file to the root of your repository with Example 4-10.
Example 4-10. .gitignore contents
node_modules/
Step 3: Set Up Storybook
Let’s add Storybook to the project so we can test and develop components in isolation (Examples 4-12 and 4-13).Example 4-12. Add the Storybook command-line tool
yarn global add @storybook/cli
Next, add Storybook to the project for development (Example 4-13).
Example 4-13. Add Storybook to the project dependencies
yarn add --dev react react-dom getstorybook
After this step, you’ll see a yarn.lock file added to your project as well as a folder called stories, which is just from the Storybook setup (Example 4-14).
Example 4-14. Cleanup example stories
rm -rf stories
You should now a folder called .storybook (and no stories folder). Lastly, edit .storybook/config.js to automatically import story.jsx files, as in Example 4-15.
Example 4-15. Configure Storybook to import all story.jsx files
import
{
configure
}
from
'@storybook/react'
;
// automatically import all story.js files
const
req
=
require
.
context
(
'../'
,
true
,
/story\.jsx$/
);
function
loadStories
()
{
req
.
keys
().
forEach
(
req
);
}
configure
(
loadStories
,
module
);
At this point, you should have a new Git repository with a Yarn package and Storybook set up and ready for development. You’re now ready to build your first atom!
Build the First Atom
At this point we should have a working development environment and have chosen the first Atom to build. That structure for the Atom library is very flat, keeping all the Atoms at the top level (Example 4-17).
Example 4-17. A Text Atom directory structure
. ├── Text │ ├── index.jsx │ └── story.jsx └── <other atoms>
This allows you to keep the stories next to the implementation, which is useful because the stories provide living documentation about the implementation. They provide useful context about how to use a component and are useful during code reviews since changes in the story often indicate an change in the component API (Examples 4-18 and 4-19).
Example 4-18. Create a space for the Text Atom
mkdir Text touch Text/index.jsx touch Text/story.jsx
Example 4-19. Add a default story to the Text/story.jsx file
// Text/story.jsx
import
React
from
'react'
;
import
{
storiesOf
}
from
'@storybook/react'
;
import
Text
from
'../index'
;
storiesOf
(
'Text'
,
module
)
.
add
(
'default'
,
()
=>
(
<
Text
>
oh
hey
!<
/Text>
));
The story.jsx file is like a test that allows you to render one component at a time in a desired configuration. With the story, you can describe the component with configuration and render the component in isolation. This allows you to play around with one component at a time and even render snapshots (which we’ll be doing very soon)!
Example 4-20. Implement the Text component
// Text/index.jsx
import
React
from
'react'
;
export
default
({
children
})
=>
<
span
>
{
children
}
<
/span>;
At this point you’ll probably want to add some custom styling so the Text matches your application’s current design. There are many good options to style components and covering them would be a book in itself. For the purpose of this report, styling will be done inline to take advantage of built-in language features and keep styles tightly scoped (Examples 4-21 and 4-22).
Example 4-21. Add stories to text Text size configurations
// Text/story.jsx
import
React
from
'react'
;
import
{
storiesOf
}
from
'@storybook/react'
;
import
Text
from
'./index'
;
storiesOf
(
'Text'
,
module
)
.
add
(
'default'
,
()
=>
(
<
Text
>
oh
hey
!<
/Text>
))
.
add
(
'size=3'
,
()
=>
(
<
Text
size
=
{
3
}
>
Rems
=
3
=
huge
<
/Text>
))
.
add
(
'size=0.5'
,
()
=>
(
<
Text
size
=
{
0.5
}
>
Rems
=
0.5
=
small
<
/Text>
));
With the Storybook API you can describe how to render the same component with different configurations in the story.jsx file. In the React Storybook API you can click through each configuration and render them one at a time (Figure 4-5).
Example 4-22. Implement configurable Text
size
import
React
from
'react'
;
export
default
({
children
,
size
=
1
,
})
=>
<
span
style
=
{{
fontSize
:
`
${
size
}
rem`
,
}}
>
{
children
}
<
/span>;
Later on, when we start building the new application, we’ll consume the new component library. With the components exported at the top level each component can be pulled in with a named import (Examples 4-23 and 4-24).
Example 4-23. Export the Text component from the top level index.js file
// index.js
export
Text
from
'./Text'
;
Example 4-24. Using a named import to consume the Text component
import
{
Text
}
from
'<my-component-library>'
;
Atom Library Setup and Build and Publish Scripts
In the next few steps we’ll prepare the repository so Atoms can be transpiled before being published to NPM. Transpiling code before publishing will make it easier to consume the Atom library in a number of different environments. The first step is to create a folder named src/ (Example 4-25) and then move index.js and Text/ into src/ (Example 4-26).
Example 4-25. Create a src directory for un-transpiled Atoms
mkdir src mv index.js src/ mv Text src/Text/
Example 4-26. Install babel CLI and env presets
yarn add --dev babel-cli\
babel-preset-env\
babel-preset-react\
babel-plugin-transform-export-extensions
We’ve configured babel to transpile the contents of the src/ directory into the lib/ directory (Examples 4-26, 4-27, and 4-31). This means we’ll also need to adjust the main entry of package.json to point to the transpiled version of the code in lib/ (Examples 4-28 through 4-31).
Example 4-27. Add a build and prepublish script to package.json
"scripts": { "storybook": "start-storybook -p 6006",- "build-storybook": "build-storybook"
+ "build-storybook": "build-storybook",
+ "build": "babel src -d lib",
+ "prepublish": "babel src -d lib"
}
Example 4-28. Set the main entry point to the transpiled index.js
"description": "A Component Library Boilerplate",- "main": "index.js",
+ "main": "lib/index.js",
"repository": "git@github.com:hharnisc/my-component-library-boilerplate.git",
Example 4-29. Add lib to .gitignore
node_modules
+lib
Example 4-30. Point .storybook/config.js to find stories in the src/ directory
// automatically import all story.js files-const req = require.context('../', true, /story\.jsx$/);
+const req = require.context('../src/', true, /story\.jsx$/);
function loadStories() {
Example 4-31. Create a .babelrc file
{
"presets"
:
[
"env"
,
"react"
],
"plugins"
:
[
"transform-export-extensions"
]
}
Set Up Snapshot Tests
Snapshot tests are effective for locking down a set of changes since they break when an unexpected output is observed. Snapshot tests are managed using Storybook Storyshots and kicked off using the Jest test runner. This unlocks a feature that creates a snapshot of each story.js file for each Atom. Getting set up requires adding Jest as a test runner and some added configuration (Example 4-33).
Example 4-33. Add Storybook Storyshots add-on
yarn add --dev @storybook/addon-storyshots jest react-test-renderer raf
Create a testSetup.js file to do test initialization before running snapshots. The Jest snapshots need to polyfill requestAnimationFrame
(Example 4-34).
Example 4-34. Polyfill requestAnimationFrame
import
'raf/polyfill'
;
Add the Jest configuration to the package.json file (Example 4-35).
Example 4-35. Jest setup configuration
"jest"
:
{
"setupFiles"
:
[
"./testSetup.js"
]
}
Create a file called snapshot.test.js and fill it with the snapshot test configuration (Example 4-36).
Example 4-36. A test that picks up all story.jsx files and runs a snapshot test
import
initStoryshots
from
'@storybook/addon-storyshots'
;
initStoryshots
({
suit
:
'Snapshots'
,
});
Add a test
script to package.json (Example 4-37) and run a test
command (Example 4-38).
Example 4-37. Configure a test script to run Jest
- "prepublish": "babel src -d lib"
+ "prepublish": "babel src -d lib",
+ "test": "jest",
+ "test:update": "jest -u"
Example 4-38. Run the test
command to create the initial snapshot
yarn run test
If you make some changes to the Text
Atom, it will cause the snapshot tests to fail. If you make a change that you’d like to keep, update the snapshots with the command in Example 4-39.
Example 4-39.
yarn run test
:update
Publish the Atom Library
At this point you’ve created a space to build and test Atoms in an isolated environment—job well done! Now it is time to share the library so your other projects can start using the Atoms. There are a couple of considerations at this point to think through. The first is that you need to decide if the library should be public or private. This is likely something you’d need to discuss with a legal team, since company policy and licensing are a factor here. The second decision is if the package will be published with a scoped namespace. The scoped namespace is usually the name of the organization, but not always. For the sake of an example, we’ll publish a public package with a scoped namespace of myorg
. The example library is MIT licensed, which is generally permissive. Let’s go ahead and publish at the root of the Atom library (Example 4-41).
Example 4-41. Publish the package to npm with the package name @myorg/my-component-library
yarn publish
Migrate the First Atom
It’s important to choose a simple component because the goal is to understand how to migrate the first Atom, and not to debug the implementation—that comes later! You can get a sense of an Atom’s complexity by thinking about the interactions a user will have with an Atom. Let’s compare some of the differences between a Button and a Text in Examples 4-42 through 4-43.
Example 4-42. A Button Atom
const
Button
=
({
onClick
,
text
})
=>
<
button
onClick
=
{
onClick
}
>
{
text
}
<
/button>;
- Needs to know what to do when someone clicks it
- Needs to know what text to display to the user
Example 4-43. A Text Atom
const
Text
=
({
text
})
=>
<
span
>
{
text
}
<
/span>;
- Needs to know what text to display to the user
The Button is much more complex than the Text component, because it needs to be passed the onClick
handler callback. It can be complex to bridge the gap between the existing and new code, especially if the existing application callback handler assumes context with this
keywords or if callback handlers aren’t easily accessible in the new application.
Starting with a simple component like a Text is a good idea, with the fewest possible user interactions.
The focus at this stage of the migration process is figuring out how to bring the Atom library into the existing application. This is important because it is likely that a similar or same method will be used to bring a Molecule into the existing application. Even though there are numerous ways that an existing application can be constructed, the basic idea for bringing the Atom library is the same. We first have to install the React and Atom libraries (Example 4-44).
Example 4-44. Install libraries as dependencies
yarn add @myorg/my-component-library react react-dom
Next, let’s create a mount point for text. Depending on the starting point, a mount point might be added in the main HTML or a template (Example 4-45).
Example 4-45. An index.html file with some text we’d like to replace
<body> <!-- Some existing text to replace --> <div id="someTextToReplace">Hello!</div> </body>
Let’s remove the element with ID someTextToReplace
and create a new element with ID myNewText
. In the myNewText
element, we’ll render the new text element with React (Example 4-46).
Example 4-46. Adding a mount point myNewText to an index.html file
<body>
<!-- Remove the element with id=someTextToReplace -->
<!-- Add a div to mount the new Text Atom -->
<div
id=
"myNewText"
></div>
</body>
If the project’s starting point is another framework, like Backbone, be sure to render the mount point before trying to render the new Atom (Example 4-47).
Example 4-47. A Backbone view rendering the mount point
Backbone
.
View
.
extend
({
render
:
()
=>
{
const
html
=
`<div id="myNewText"></div>`
;
this
.
setElement
(
$
(
html
)
);
return
this
;
}
});
After creating a mount point for the new Atom, its time to import the Text Atom and render it along with the existing UI (Example 4-48).
It is important to note that the mount point must exist before React can render the new Text component. Also, if the mount point is re-rendered the new Text component must be rendered again.
Example 4-48. A Backbone view responding to model changes and rendering a React component
import
Backbone
from
'backbone'
;
import
React
from
'react'
;
import
ReactDOM
from
'react-dom'
;
import
{
Text
}
from
'@myorg/my-component-library'
;
Backbone
.
View
.
extend
({
initialize
:
()
=>
{
// call render when model changes
this
.
model
.
on
(
'change'
,
this
.
render
,
this
);
},
render
:
()
=>
{
const
html
=
`<div id="myNewText"></div>`
;
this
.
setElement
(
$
(
html
)
);
ReactDOM
.
render
(
React
.
createElement
(
Text
,
{
children
:
'Hello!'
}),
document
.
getElementById
(
'myNewText'
)
);
return
this
;
}
});
This pattern works well because React can be used alongside other frameworks. Since Atoms don’t assume context, it is possible to pass data directly from the Backbone model if needed (Example 4-49).
Example 4-49. Rendering an Atom with data from a Backbone model
ReactDOM
.
render
(
React
.
createElement
(
Text
,
{
children
:
this
.
model
.
get
(
'message'
),
}),
document
.
getElementById
(
'myNewText'
)
);
A New Application Base
At this point, the foundations for the Atom library have been laid out, and the first Atom has been migrated into the existing application. Now it is time to start building the new application, which is also the development environment for the Molecules. This section will go through setting up a monorepo with packages that work together to form an Organism.
Architecture
All of an application’s functionality is organized into packages that perform various tasks. There are a few special packages like the backend
, frontend
, and store
; however the rest of the packages will likely be Molecules. Each of the Molecules are tied together in the frontend
package and communicate via the store
package with a Redux store, via the observer pattern. This means that all events that get triggered are sent to all packages, and it is up to the package to listen and handle a specific event. This keeps the packages loosely coupled to better handle change (Example 4-50).
Example 4-50. Example application package structure
. ├── lerna.json ├── package.json └── packages ├── backend │ └── package.json ├── frontend │ └── package.json ├── molecule-x │ └── package.json ├── molecule-y │ └── package.json └── store └── package.json
Backend
The backend
package is responsible for serving static files and any data that the frontend
package needs. Under the root route /
, the backend serves the index.html file (Example 4-51), which provides the mount point for the frontend
package and pulls in the frontend
package bundle.
Example 4-51. An example index.html file
<!DOCTYPE html>
<html>
<head>
<title>
My Monorepo Boilerplate</title>
</head>
<body>
<!-- mount point for frontend package -->
<div
id=
"root"
></div>
<!-- including the frontend bundle -->
<script
src=
"/static/bundle.js"
></script>
</body>
</html>
The webpack configuration lives in the backend
package and is used to bundle and serve the bundled frontend
package. During development it is also used to serve an in-memory bundle with webpack-dev-middleware
to make it easy to hook up hot module replacement.
Pick One Communication Method
There are numerous ways to serve data from the backend—REST, WebSockets, GraphQL and RPC to name a few. While the best choice depends on the problem you’re solving, it is often better to pick one. Mixing and matching communication methods increases complexity.
Frontend
The frontend
package is the entry point for webpack to build all client-side packages. The top-level application initializes the store and hooks the store data into the Molecules (Example 4-52).
Example 4-52. An example frontend entry point file
import
React
from
'react'
;
import
{
render
}
from
'react-dom'
;
import
{
Provider
}
from
'react-redux'
;
import
{
ConnectedRouter
as
Router
}
from
'react-router-redux'
;
import
createStore
,
{
history
}
from
'my-monorepo-boilerplate-store'
;
import
App
from
'./components/App'
;
const
store
=
createStore
();
store
.
dispatch
({
type
:
'APP_INIT'
,
});
render
(
<
Provider
store
=
{
store
}
>
<
Router
history
=
{
history
}
>
<
App
/>
<
/Router>
<
/Provider>,
document
.
getElementById
(
'root'
),
);
The App
component would likely contain high-level navigation elements and handle client-side routes. When a user visits a particular route, the client-side router would chose the right Molecule or set of Molecules to display.
Example 4-53. An example App
component
import
React
from
'react'
;
import
{
Route
,
Switch
}
from
'react-router'
;
import
AppSidebar
from
'my-monorepo-boilerplate-app-sidebar'
;
import
ItemPage
from
'my-monorepo-boilerplate-item-page'
;
import
ListPage
from
'my-monorepo-boilerplate-list-page'
;
export
default
()
=>
<
div
>
<
AppSidebar
/>
<
Switch
>
<
Route
path
=
{
'/item/:id'
}
component
=
{
ItemPage
}
/>
<
Route
component
=
{
ListPage
}
/>
<
/Switch>
<
/div>;
Store
The store
package is responsible for holding client state and bringing together all the reducers and middlewares (Example 4-54). It utilizes Redux to enable packages to communicate via actions, which makes the packages loosely coupled. Loosely coupled packages are an advantage here because they make it easy for features to be worked on in parallel or be completely rewritten as needed. When a package triggers an action, the action is sent to all packages (including itself). Each action can be ignored or handled by the package.
Example 4-54. A store with reducers and middleware
import
{
createStore
,
applyMiddleware
,
combineReducers
,
}
from
'redux'
;
import
{
middleware
as
appSidebarMiddleware
,
reducer
as
appSidebarReducer
,
storeKey
as
appSidebarStoreKey
,
}
from
'my-monorepo-boilerplate-app-sidebar'
;
import
{
middleware
as
itemPageMiddleware
,
reducer
as
itemPageReducer
,
storeKey
as
itemPageStoreKey
,
}
from
'my-monorepo-boilerplate-item-page'
;
import
{
middleware
as
listPageMiddleware
,
reducer
as
listPageReducer
,
storeKey
as
listPageStoreKey
,
}
from
'my-monorepo-boilerplate-item-page'
;
export
default
(
initialstate
=
{})
=>
createStore
(
combineReducers
({
[
appSidebarStoreKey
]
:
appSidebarReducer
,
[
itemPageStoreKey
]
:
itemPageReducer
,
[
listPageStoreKey
]
:
listPageReducer
,
}),
initialstate
,
applyMiddleware
(
appSidebarMiddleware
,
itemPageMiddleware
,
listPageMiddleware
,
),
);
Reducer versus middleware
Actions can be handled in a couple different ways, depending on the needs of the package. When a handled action should change the state of a Molecule, it should be handled in a reducer. An example of this would be showing a loading screen while waiting for some data to complete fetching (Example 4-55).
Example 4-55. An example reducer that manages a loading state
export
default
(
state
=
{
loading
:
false
},
action
)
=>
{
switch
(
action
.
type
)
{
case
`user_fetch_start`
:
return
{
...
state
,
loading
:
true
,
};
case
`user_fetch_success`
:
case
`user_fetch_fail`
:
return
{
...
state
,
loading
:
false
,
};
default
:
return
state
;
}
};
The reducer only describes how to change the state when a given action occurs. If a side effect is desired when an action occurs, like fetching data from the backend, it should be done in middleware (Example 4-56).
Side Effects and State Mutations
As a general guideline, side effects should be done in middleware and state mutations should be done in reducers.
Example 4-56. A middleware that fetches user data and dispatches actions
import
fetch
from
'isomorphic-fetch'
;
export
default
({
dispatch
})
=>
next
=>
async
(
action
)
=>
{
next
(
action
);
if
(
action
.
type
===
'user_fetch_start'
)
{
try
{
const
response
=
await
fetch
(
'/api/users'
);
const
users
=
await
response
.
json
();
dispatch
({
type
:
'user_fetch_success'
,
users
,
});
}
catch
(
error
)
{
dispatch
({
type
:
'user_fetch_error'
,
error
,
});
}
}
};
Molecule
Molecule packages will likely be the most common type of package in your application. They have a similar structure since each Molecule package contains descriptions of how to handle actions from the Redux store and how to present the data to the user (Example 4-57).
Example 4-57. An example Sidebar Molecule
. ├── components │ └── Sidebar │ ├── index.jsx │ └── story.jsx ├── index.js ├── middleware.js ├── package.json └── reducer.js
Components
These are React presentational components that are concerned with how things look. Each component is functional and stateless, unless it needs life-cycle hooks or performance optimizations (Example 4-58).
Example 4-58. A Sidebar component
. └── components └── Sidebar ├── index.jsx ├── story.jsx └── test.jsx(
optional)
Each component should have an index.jsx file that describes how a component looks for each configuration and a story.jsx file that renders the component in each configuration. If a component needs life-cycle hooks, it is likely some unit tests will be needed for good test coverage, which would live in an optional test.jsx file.
Reducer.js
Most Molecules will need data to render the presentational components. In this architecture each Molecule has its own bucket of data within the state tree. In the store
package we make sure each of the reducers gets hooked up under their own unique bucket with a different key. After the reducer is hooked up in the store, it can start receiving actions. If there is an action that the Molecule is interested in, the Molecule can handle the action and modify its own bucket of state. The reducer.js file exports the Redux reducer as the default export, as well as the actions and action creators the Molecule needs (Example 4-59).
Example 4-59. Sidebar Molecule reducer, actions, and action creators
export
const
actionTypes
=
{
NAV_ITEM_CLICKED
:
'NAV_ITEM_CLICKED'
,
};
export
default
(
state
=
{
loading
:
false
},
action
)
=>
{
switch
(
action
.
type
)
{
case
`user_fetch_start`
:
return
{
...
state
,
loading
:
true
,
};
case
`user_fetch_success`
:
case
`user_fetch_fail`
:
return
{
...
state
,
loading
:
false
,
};
default
:
return
state
;
}
};
export
const
actions
=
{
navItemClicked
:
({
id
})
=>
({
type
:
NAV_ITEM_CLICKED
,
id
,
}),
};
Middleware.js
Redux middleware should be used when an action should trigger a side effect. Some examples of side effects:
- AJAX request
- Triggering another action
- Console logging actions
- Crash reporting
Middleware is powerful, because it can trigger code for every action and stop an action from propagating under certain conditions. The middleware provides an extension point between dispatching an action and a reducer (Example 4-60).
Example 4-60. A middleware that makes an API request and dispatches another action
import fetch from 'isomorphic-fetch'; export default ({ dispatch }) => next => async (action) => { next(action); if (action.type === 'user_fetch_start') { try { const response = await fetch('/api/users'); const users = await response.json(); dispatch({ type: 'user_fetch_success', users, }); } catch (error) { dispatch({ type: 'user_fetch_error', error, }); } } };
Index.js
This is the main entry point for a Molecule and the index.js file exposes the API to the rest of the system (Example 4-61).
Example 4-61. AppSidebar Molecule index.js file
import
{
connect
}
from
'react-redux'
;
import
AppSidebar
from
'./components/AppSidebar'
;
import
{
actions
}
from
'./reducer'
;
export
default
connect
(
state
=>
({
loggedIn
:
state
.
example
.
loggedIn
,
}),
dispatch
=>
({
onProfileClick
:
id
=>
dispatch
(
actions
.
navItemClicked
({
id
,
})),
}),
)(
AppSidebar
);
export
reducer
,
{
actions
,
actionTypes
}
from
'./reducer'
;
export
middleware
from
'./middleware'
;
The index.js file usually exports the following:
- Default export: a container component
reducer
actions
actionTypes
middleware
The only requirement is that the default export container component is exported: the rest of the exports are optional. It is highly recommended that you keep these names consistent across your Molecules. Keeping a consistent API maximizes portability and make it easier to work with Molecules.
Package.js
Each Molecule is a JavaScript package and each has its own package.js file (Example 4-62). Molecules can depend on external packages as well as other local Molecule packages. Utilizing Yarn workspaces allows local packages to be depended on without needing to download them from NPM.
Example 4-62. App Sidebar package.json
{
"name"
:
"my-monorepo-boilerplate-app-sidebar"
,
"version"
:
"1.0.0"
,
"description"
:
"My Monorepo Boilerplate App Sidebar Package"
,
"main"
:
"index.jsx"
,
"author"
:
"hharnisc@gmail.com"
,
"license"
:
"MIT"
,
"private"
:
false
,
"dependencies"
:
{
"my-monorepo-boilerplate-async-data-fetch"
:
"0.0.1"
,
"lodash"
:
"4.17.4"
}
}
Create a Git Repository
Here’s an example showing how to create a Git repository on GitHub: http://bit.ly/2LZy50q.
Please note that the hharnisc/my-monorepo-boilerplate repository is an example repository that follows along with the my-new-monorepo project we’ll be building step by step.
Clone the Git Repository
Clone the Git repository by executing the following command in the terminal:
git
clone
git@github.com:
<
my-account
>
/
<
my-new-monorepo
>
.git
Initialize Package
To initialize the package, run the following commands:
cd
my-new-monorepo
yarn
init
question
name
(
my-new-monorepo
)
:
question
version
(
1.0.0
)
:
0.0.1
question
description:
My
New
Monorepo
question
entry
point
(
index.js
)
:
question
git
repository:
https://github.com/
<
my-account
>
/
<
my-new-monorepo
>
question
author:
YOU
:D
question
license
(
MIT
)
:
MIT
question
private:
no
success
Saved
package.json
Initialize Tooling
When working with a monorepo split into several packages, it is helpful to have tools to manage dependencies efficiently and run tests. We’ll add the following tools:
- Lerna
- Detect package changes and automatically update versions.
- Yarn Workspaces
- Manage package dependencies efficiently.
- Jest
- Run each package’s tests.
Add Lerna
Use Example 4-64 to initialize Lerna to detect package changes and automatically update versions.
Example 4-64. Initialize Lerna
lerna init
This should create a lerna.json file that contains information about the Lerna configuration settings. It also adds Lerna as a devDependency
of the package. For documentation purposes, let’s add a README.md file in the packages folder (Example 4-65). As we add more packages, we can use the README file to describe what each package does.
Example 4-65. Add a packages directory and initialize a README.md file
mkdir packagesecho
"# Packages\n"
> packages/README.md
Add Yarn Workspaces
Although Lerna can manage initializing and installing package dependencies, there are some rough edges when using advanced features like hoisting. Hoisting helps de-duplicate shared dependencies across packages and reduces setup and build times dramatically as the number of packages increase. Yarn Workspaces also solve the dependency duplication problem without the Lerna hoisting issues (Example 4-67). This means adding Yarn Workspaces can be skipped, but is not recommended.
Example 4-67. Modify lerna.json to use Yarn Workspaces
{ "lerna": "2.5.1", "packages": [ "packages/*" ],- "version": "0.0.0"
+ "version": "0.0.0",
+ "npmClient": "yarn",
+ "useWorkspaces": true
}
Add Jest
There are several testing options available with different trade-offs. Jest is chosen here because it brings features like code coverage, parallelized test execution, minimal configuration, mocking, and snapshot testing (Examples 4-69 and 4-70).
Example 4-69. Add Jest to the project
yarn add --dev jest
Example 4-70. Add a .gitignore file
node_modules
Create a Backend Package
The backend
package is responsible for serving the client JavaScript bundle (which we’ll create in following sections) and providing endpoints to retrieve data to support the frontend. The first step is to initialize the backend as a package of the application and configure the root package to install subpackage dependencies using Yarn Workspaces (Examples 4-72 through 4-75).
Example 4-72. Initialize backend package
cd
packages mkdir backendcd
backend yarn initcd
../..
Example 4-73. Update package.json to install package dependencies with Yarn Workspaces
"license": "MIT",+ "scripts": {
+ "package-install": "yarn"
+ },
+ "workspaces": [
+ "packages/*"
+ ],
+ "private": true,
"devDependencies": {
Example 4-74. Add express as a dependency to packages/backend/package.json
"license": "MIT",- "private": false
+ "private": false,
+ "dependencies": {
+ "express": "^4.16.2"
+ }
}
Example 4-75. Ignore lerna-debug.log files in .gitignore
node_modules
+lerna-debug.log
Now it’s time to run the command to install all package dependencies. The package-install
command will come in handy later when any package dependencies get updated. Be sure the run the command in Example 4-76 at the root of the project.
Example 4-76. Install all package dependencies
yarn run package-install
The start script at the top level package.json file should be configured to run the backend’s package.json file (Examples 4-77 through 4-79). The backend
package is the entry point for the entire application, since later we’ll configure it to serve the frontend code from the other packages.
Example 4-77. Add a start script to package.json
"scripts": {
+ "start": "cd ./packages/backend/ && yarn start",
"package-install": "yarn"
},
Example 4-78. Add a start script to packages/backend/package.json
"private": false,+ "scripts": {
+ "start": "node ."
+ },
"dependencies": {
Example 4-79. Create a packages/backend/index.js file
const express = require('express') const app = express() app.get('*', (req, res) => res.send('Hello World')) app.listen(3000)
Now we’re ready to try to run the backend server for the first time (Example 4-80).
Example 4-80. Start the backend server
yarn run start
Navigating to localhost:3000 should show the text Hello World
.
Create a Frontend Package
The frontend
package is the entry point for all client-side code and all dependencies will be bundled together using webpack (Example 4-82). We’ll use React and Redux to manage the view and data to drive the view.
Example 4-82. Initialize the frontend package
cd
packages mkdir frontendcd
frontend yarn initcd
../..
Let’s set up Storybook so it can use it for each package with a UI (Examples 4-83 through 4-85).
Example 4-83. Install the React version of Storybook
yarn add -DW @storybook/addon-storyshots @storybook/react react react-dom
Example 4-84. Add a script to package.json to start Storybook
"scripts": { "start": "cd ./packages/backend/ && yarn start",- "package-install": "yarn"
+ "package-install": "yarn",
+ "start-storybook": "start-storybook -p 9000"
},
Example 4-85. Configure Storybook
mkdir .storybook touch .storybook/config.js touch .storybook/webpack.config.js
The Storybook configuration will automatically load all story.jsx files in each package components directory. This minimizes the need to update the configuration over time. We’ll also allow for setting an environment variable to limit the scope to one package for development purposes (Examples 4-86 and 4-87).
Example 4-86. Edit .storybook/config.js
import
{
configure
}
from
'@storybook/react'
// __PACKAGES__ is defined via plug-in in ./webpack.config.js
// because require.context needs static values. This allows us to use
// an environment variable to narrow the scope of the packages loaded.
const
req
=
require
.
context
(
__PACKAGES__
,
true
,
/components\/.*\/story\.jsx$/
)
const
loadStories
=
()
=>
req
.
keys
().
forEach
(
req
)
configure
(
loadStories
,
module
)
Example 4-87. Edit .storybook/webpack.config.js
const
webpack
=
require
(
'webpack'
)
module
.
exports
=
{
// define a global static string called __PACKAGES__
plugins
:
[
new
webpack
.
DefinePlugin
({
__PACKAGES__
:
JSON
.
stringify
(
`../packages/
${
process
.
env
.
PACKAGE
||
''
}
`
)
})]
}
To run Storybook for all packages:
yarn run start-storybook
To run Storybook for one package:
PACKAGE
=
frontend yarn run start-storybook
Since the frontend
package is the entry point to the application, let’s start building the top level App
component (Examples 4-88 through 4-90). Each package we create has a directory named components
that holds all of the React components and the associated story file.
Example 4-88. Create the App component and story
mkdir packages/frontend/components/App
cd
packages/frontend/components/App
touch index.jsx
touch story.jsx
Example 4-89. Edit the packages/frontend/components/App/story.jsx file
import
React
from
'react'
import
{
storiesOf
}
from
'@storybook/react'
import
App
from
'./index'
storiesOf
(
'App'
,
module
)
.
add
(
'should render application'
,
()
=>
(
<
App
/>
))
Example 4-90. Edit the packages/frontend/components/App/index.jsx file
import
React
from
'react'
export
default
()
=>
<
div
>
My
Application
<
/div>
The top-level App
component needs to be rendered with react-dom; let’s create an index.jsx file in the frontend
package and adjust the package.json file to point to the new frontend
package entry point (Examples 4-91 and 4-92).
Example 4-91. Create a packages/frontend/index.jsx file
import
React
from
'react'
import
{
render
}
from
'react-dom'
import
App
from
'./components/App'
render
(
<
App
/>
,
document
.
getElementById
(
'root'
),
)
Example 4-92. Set the package entry point in packages/frontend/package.json
"description": "My Monorepo Boilerplate Frontend Package",- "main": "index.js",
+ "main": "index.jsx",
"author": "hharnisc@gmail.com",
For the last step of this section let’s configure the backend
package to bundle and serve the frontend
package with webpack middleware. Lets start by installing babel dependencies (Example 4-93).
Example 4-93. Babel loader and dependencies for webpack configuration
yarn add -DW babel-loader\
babel-plugin-transform-export-extensions\
babel-preset-env\
babel-preset-react
Configure babel to use the env and react presets as well as the transform-export-extensions. And create a webpack configuration file to bundle the frontend
package with Babel (Examples 4-94 and 4-95).
Example 4-94. Create a .babelrc file
{
"presets"
:
[
"env"
,
"react"
],
"plugins"
:
[
"transform-export-extensions"
]
}
Example 4-95. Create packages/backend/webpack.config.js> webpack configuration file
module
.
exports
=
{
context
:
__dirname
,
entry
:
[
'../frontend/index.jsx'
,
],
output
:
{
path
:
__dirname
,
filename
:
'bundle.js'
,
publicPath
:
'/static/'
,
},
resolve
:
{
extensions
:
[
'.js'
,
'.json'
,
'.jsx'
],
},
module
:
{
rules
:
[
{
test
:
/\.jsx?$/
,
use
:
{
loader
:
'babel-loader'
,
},
},
],
},
};
Let’s create an index.html file with an anchor point for the frontend
package and configure the server to serve the bundled frontend code using webpack-dev-middleware (Examples 4-96 and 4-97).
Example 4-96. Create packages/backend/index.html file
<!DOCTYPE html>
<html>
<head>
<title>
My Monorepo Boilerplate</title>
<style>
html
,
body
,
#root
{
height
:
100%
;
margin
:
0
;
}
</style>
</head>
<body>
<div
id=
"root"
></div>
<script
src=
"/static/bundle.js"
></script>
</body>
</html>
Example 4-97. Edit the packages/backend/index.js file
+const { readFileSync } = require('fs')
+const { join } = require('path')
const express = require('express') const app = express()-app.get('*', (req, res) => res.send('Hello World'))
+
+const webpack = require('webpack')
+const config = require('./webpack.config')
+const webpackMiddleware = require('webpack-dev-middleware')
+const compiler = webpack(config)
+app.use(webpackMiddleware(compiler, {
+ publicPath: config.output.publicPath,
+}))
+
+const html = readFileSync(join(__dirname, 'index.html'), 'utf8')
+
+app.get('*', (req, res) => res.send(html))
app.listen(3000)
Migrating the First Molecule
After creating the frontend
and backend
packages, the next step is to build the first Molecule. At this point the focus should be on building a library of quality Atoms to stitch together into a Molecule, and passing the right data into the new Atom from the existing application.
Choose the First Molecule
The Molecule you decide to build first depends on where the existing product is within the product life-cycle and the amount of tolerable risk. There are a couple of viable options that can work well in different scenarios. If the product is in a place where new features are being developed, the next new feature might be a good candidate. If the product changes infrequently, it might be good to choose something on a settings page. There are two main variables to consider when choosing the first Molecule: the product’s level of risk tolerance and if you’ll build a new or existing feature (Table 4-1).
New feature | Existing feature | |
---|---|---|
High risk tolerance | Next feature | Pick a small feature that is close to the data (a screen that displays item details) |
Low risk tolerance | Wait for a small and low visibility feature | Pick a feature with low usage/visibility (settings screen) |
It is important to pick a Molecule that doesn’t harm the existing product cycle, which minimizes impact to the business.
Build the First Molecule
Let’s say you’ve chosen to build the application sidebar Molecule first, since the application can handle a fair amount of risk and you’re not adding new features this week. Figure 4-6 is a schematic diagram of the sidebar you’re planning to build.
Identify the Atoms
First, break down the design into a bunch of Atoms that can be saved in the Atom library so they can be reused later. This will increase the chances the work done now will be reused tomorrow and also make the work more parallelizable.
Figure 4-7 is a diagram with the sidebar Molecule’s Atoms outlined. It can be broken down into four reusable Atoms.
The five reusable Atoms are:
- Text
- The title on the AppSidebar
- List
- A list of navigation elements
- ListItem
- One element that holds a navigation item
- Link
- An Anchor to a part of the application
- Image
- An image to display the user’s profile
Build the Atoms
If we look back at the Atom library, the Text component has already been created and has a configurable size. This means the Text component is already done, and it can be used out of the box! Let’s first focus on implementing the Link component (Examples 4-99 through 4-101).
Example 4-99. Initialize the Link component
cd
my-component-library/src
mkdir Link
touch Link/index.jsx
touch Link/story.jsx
Example 4-100. Create a Link story.jsx file
import
React
from
'react'
;
import
{
storiesOf
}
from
'@storybook/react'
;
import
Link
from
'./index'
;
storiesOf
(
'Link'
,
module
)
.
add
(
'default'
,
()
=>
(
<
Link
href
=
{
'https://www.google.com/search?q=monorepo'
}
>
Search
-
Monorepo
<
/Link>
))
.
add
(
'unstyled'
,
()
=>
(
<
Link
href
=
{
'https://www.google.com/search?q=css'
}
unstyled
>
(
unstyled
)
Search
-
CSS
<
/Link>
));
Example 4-101. Create a Link index.jsx file
import
React
from
'react'
;
export
default
({
children
,
href
,
unstyled
,
})
=>
{
let
style
=
{};
if
(
unstyled
)
{
style
=
{
textDecoration
:
'none'
,
outline
:
'none'
,
color
:
'inherit'
,
};
}
return
(
<
a
href
=
{
href
}
style
=
{
style
}
>
{
children
}
<
/a>
);
};
The Link component in the current implementation can either be a standard Link or unstyled. We’ll need the unstyled version of the Link to satisfy the design.
Let’s build the ListItem Atom to hold each of the Links. Building the ListItem before the List is ideal since we’ll be able to use the ListItem in the List (Examples 4-103 through 4-105).
Example 4-103. Initialize the ListItem Atom
cd
my-component-library/src
mkdir ListItem
touch ListItem/index.jsx
touch ListItem/story.jsx
Example 4-104. Create a ListItem story.jsx file
import
React
from
'react'
;
import
{
storiesOf
}
from
'@storybook/react'
;
import
ListItem
from
'./index'
;
storiesOf
(
'ListItem'
,
module
)
.
add
(
'default'
,
()
=>
(
<
ListItem
>
Banana
<
/ListItem>
));
Example 4-105. Create a ListItem index.jsx file
import
React
from
'react'
;
const
style
=
{
listStyle
:
'none'
,
};
export
default
({
children
})
=>
<
li
style
=
{
style
}
>
{
children
}
<
/li>;
The ListItem Atom removes the standard li
style so the bullet doesn’t display by default. If the standard style is needed, it can be added as a configuration property. Let’s use the ListItem to build the List Atom (Examples 4-107 through 4-109).
Example 4-107. Initialize the List Atom
cd
my-component-library/src
mkdir List
touch List/index.jsx
touch List/story.jsx
Example 4-108. Create a List story.jsx file
import
React
from
'react'
;
import
{
storiesOf
}
from
'@storybook/react'
;
import
List
from
'./index'
;
storiesOf
(
'List'
,
module
)
.
add
(
'default'
,
()
=>
(
<
List
items
=
{[
'apple'
,
'banana'
,
'orange'
]}
/>
));
Example 4-109. Create a List index.jsx file
import
React
from
'react'
;
import
ListItem
from
'../ListItem'
;
export
default
({
items
,
})
=>
<
ul
style
=
{{
padding
:
0
,
margin
:
0
,
}}
>
{
items
.
map
((
item
,
i
)
=>
<
ListItem
key
=
{
i
}
>
{
item
}
<
/ListItem>)}
<
/ul>;
The Image Atom needs to know which image to load and how big the image should be. Let’s create an Image that has a configurable src, height, and width (Examples 4-111 and 4-113).
Example 4-111. Initialize the Image Atom
cd
my-component-library/src
mkdir Image
touch Image/index.jsx
touch Image/story.jsx
Example 4-112. Create a story.jsx file
import
React
from
'react'
;
import
{
storiesOf
}
from
'@storybook/react'
;
import
Image
from
'./index'
;
storiesOf
(
'Image'
,
module
)
.
add
(
'default'
,
()
=>
(
<
Image
src
=
{
'http://via.placeholder.com/300x300'
}
/>
))
.
add
(
'height=5rem'
,
()
=>
(
<
Image
src
=
{
'http://via.placeholder.com/300x300'
}
height
=
{
'5rem'
}
/>
))
.
add
(
'width=10rem'
,
()
=>
(
<
Image
src
=
{
'http://via.placeholder.com/300x300'
}
width
=
{
'10rem'
}
/>
))
.
add
(
'height=5rem,width=10rem'
,
()
=>
(
<
Image
src
=
{
'http://via.placeholder.com/300x300'
}
height
=
{
'5rem'
}
width
=
{
'10rem'
}
/>
));
Example 4-113. Create an index.jsx file
import
React
from
'react'
;
export
default
({
src
,
height
,
width
,
})
=>
<
img
src
=
{
src
}
style
=
{{
height
,
width
,
}}
/>
;
Now we’ve got all the Atoms needed to create the AppSidebar Molecule. We’ll need to export the new Atoms and then publish the library to NPM (Examples 4-115 and 4-116).
Example 4-115. Add the Link, List, and ListItem Atoms to src/index.js
+export Link from './Link';
+export List from './List';
+export ListItem from './ListItem';
export Text from './Text';
Example 4-116. Publish the Atom library to NPM
# in the root of the component library
npm publish
Atom Library Transpiler Configuration
The publish step invokes the prepublish
command and transpiles the Atom library before publishing. Transpiling the code before publishing ensures maximum portability; choosing to not transpile the code will require babel to be configured in projects that consume the Atom library. If a specific range of browsers are being targeted, it’s possible to modify the env-preset in .babelrc to get better compatibility or better performance.
Assemble Atoms into a Molecule
Now that all of the Atoms have been built, it is time to put them together to form the AppSidebar Molecule. The Atom library needs to be pulled into the application as a devDependency
since the Atom library will be used across several packages (Example 4-118).
Example 4-118. Add the Atom library to the application
# within the root of the application
yarn add -DW <your atom library>
Let’s initialize a new package to hold the AppSidebar Molecule and use the Atoms to assemble the AppSidebar stateless component (Examples 4-119 and 4-120).
Example 4-119. Initialize the AppSidebar Molecule
cd
packages mkdir app-sidebarcd
app-sidebar yarn init# fill in package details
Example 4-120. Stub in the AppSidebar component
# while still in the app-sidebar package
mkdir -p components/AppSidebarcd
components/AppSidebar touch index.jsx touch story.jsx
Now that the AppSidebar component is stubbed out, lets implement the UI. While implementing the AppSidebar component it will be helpful to use Storybook for development (Examples 4-121 through 4-124).
Example 4-121. Start Storybook for only the app-sidebar
#in the root of the Application
PACKAGE
=
app-sidebar yarn run start-storybook
Example 4-122. Create storybook/preview-head.html to utilize the entire Storybook screen
<style>
#root
,
html
,
body
{
height
:
100%
;
padding
:
0
;
margin
:
0
;
}
</style>
Example 4-123. Implement the packages/app-sidebar/components/AppSidebare/story.jsx file
import
React
from
'react'
import
{
storiesOf
}
from
'@storybook/react'
import
AppSidebar
from
'./index'
storiesOf
(
'AppSidebar'
,
module
)
.
add
(
'should render AppSidebar'
,
()
=>
(
// AppSidebar fills the parent container
// the parent container sets the size of the AppSidebar
<
div
style
=
{{
width
:
'300px'
,
height
:
'100%'
,
}}
>
<
AppSidebar
profile
=
{{
name
:
'Bobson Dugnutt'
,
image
:
'https://hharnisc.github.io/images/me3.jpg'
,
}}
/>
<
/div>
))
Example 4-124. Implement the packages/app-sidebar/components/AppSidebare/index.jsx file
import
React
from
'react'
import
{
Link
,
List
,
ListItem
,
Text
,
Image
,
}
from
'my-component-library-boilerplate'
;
const
menuItems
=
[
{
name
:
'Packages'
,
href
:
'/packages'
,
},
{
name
:
'Settings'
,
href
:
'/settings'
,
},
];
export
default
({
profile
,
})
=>
<
nav
style
=
{{
height
:
'100%'
,
width
:
'100%'
,
padding
:
'1rem'
,
background
:
'#F2F2F2'
,
boxSizing
:
'border-box'
,
display
:
'flex'
,
flexDirection
:
'column'
,
}}
>
<
Text
size
=
{
2
}
>
My
Monorepo
App
<
/Text>
<
div
style
=
{{
flexGrow
:
1
,
}}
>
<
List
items
=
{
menuItems
.
map
((
item
)
=>
<
Link
href
=
{
item
.
href
}
unstyled
>
<
div
style
=
{{
paddingTop
:
'1rem'
,
}}
>
<
Text
size
=
{
1.5
}
>
{
item
.
name
}
<
/Text>
<
/div>
<
/Link>)}
/>
<
/div>
<
div
style
=
{{
display
:
'flex'
,
alignItems
:
'center'
,
}}
>
<
Image
src
=
{
profile
?
profile
.
image
:
undefined
}
height
=
{
'2rem'
}
width
=
{
'2rem'
}
/>
<
div
style
=
{{
flexGrow
:
1
,
paddingLeft
:
'1rem'
,
}}
>
<
Text
>
{
profile
?
profile
.
name
:
undefined
}
<
/Text>
<
/div>
<
/div>
<
/nav>
Connect the Atom with State
The AppSidebar presentational components are implemented and need to be passed user data to render. That user data will come from the backend
package, fetched with a request and then stored in Redux. Redux middleware within the AppSidebar package will trigger the request and fire an action when the user data arrives. The action that includes the fetched user data payload will be used to render the AppSidebar presentation component via container. Let’s start by implementing the action types, actions, and reducer (Example 4-126).
Example 4-126. Create a packages/app-sidebar/reducer.js file
export
const
actionTypes
=
{
FETCH_USER_START
:
'FETCH_USER_START'
,
FETCH_USER_SUCCESS
:
'FETCH_USER_SUCCESS'
,
FETCH_USER_FAIL
:
'FETCH_USER_FAIL'
,
};
const
initialState
=
{
user
:
null
,
error
:
null
,
loading
:
false
,
};
export
default
(
state
=
initialState
,
action
)
=>
{
switch
(
action
.
type
)
{
case
actionTypes
.
FETCH_USER_START
:
return
{
...
state
,
...
initialState
,
// clear user and error states
loading
:
true
,
};
case
actionTypes
.
FETCH_USER_SUCCESS
:
return
{
...
state
,
user
:
action
.
user
,
loading
:
false
,
};
case
actionTypes
.
FETCH_USER_FAIL
:
return
{
...
state
,
error
:
action
.
error
,
loading
:
false
,
};
default
:
return
state
;
}
};
export
const
actions
=
{
fetchUser
:
()
=>
({
type
:
actionTypes
.
FETCH_USER_START
,
}),
fetchUserSuccess
:
({
user
})
=>
({
type
:
actionTypes
.
FETCH_USER_SUCCESS
,
user
,
}),
fetchUserFail
:
({
error
})
=>
({
type
:
actionTypes
.
FETCH_USER_FAIL
,
error
,
}),
};
The reducer exports actionTypes
, actions
, and the default export is the reducer
. Each of these manage the user data fetch life cycle. When a fetch starts, a loading state is set to true, so the UI can provide feedback that a request is in flight. When a fetch completes, it will either have the user data and trigger the success action or will have an error and trigger the fail action. Now let’s create some middleware to trigger a fetch to backend (Examples 4-127 through 4-128).
Example 4-127. Add isomorphic-fetch as a dependency
# in the root of the application
yarn add -DW isomorphic-fetch
Example 4-128. Create a packages/app-sidebar/middleware.js file
import
fetch
from
'isomorphic-fetch'
;
import
{
actionTypes
,
actions
,
}
from
'./reducer'
;
export
default
({
dispatch
})
=>
next
=>
async
action
=>
{
next
(
action
);
switch
(
action
.
type
)
{
case
'APP_INIT'
:
dispatch
(
actions
.
fetchUser
());
break
;
case
actionTypes
.
FETCH_USER_START
:
try
{
const
response
=
await
fetch
(
'/api/user'
);
const
user
=
await
response
.
json
();
dispatch
(
actions
.
fetchUserSuccess
({
user
}));
}
catch
(
error
)
{
dispatch
(
actions
.
fetchUserFail
({
error
:
error
.
message
}));
}
break
;
default
:
break
;
}
};
The app-sidebar
middleware uses fetch to make a request to the backend. If the response contains the user data, a fetchUserSuccess
action is triggered, but if an error occurs the fetchUserFail
action is triggered. Let’s create the container for the AppSidebar component to prepare for when we add the Redux store (Examples 4-129 through 4-130).
Example 4-129. Add Redux and React Redux packages
# in the root of the application
yarn add -DW redux react-redux
Example 4-130. Create a packages/app-sidebar/index.js file
import
{
connect
}
from
'react-redux'
;
import
AppSidebar
from
'./components/AppSidebar'
;
export
const
storeKey
=
'AppSidebar'
;
export
default
connect
(
state
=>
({
profile
:
state
[
storeKey
].
user
?
state
[
storeKey
].
user
.
profile
:
undefined
,
}),
)(
AppSidebar
);
export
reducer
,
{
actions
,
actionTypes
}
from
'./reducer'
;
export
middleware
from
'./middleware'
;
The index.js file is the entry point for the app-sidebar
package and exports everything that other packages need. The next step is to create the store
package and hook up the app-sidebar
reducer and middleware (Examples 4-131 through 4-134).
Example 4-131. Initialize the store package
cd
packages mkdir storecd
store yarn initcd
../..
Example 4-132. Add the app-sidebar package as a dependency of the store package
{ "name": "my-monorepo-boilerplate-store", "version": "0.0.0", "description": "Store Package", "main": "index.js", "author": "hharnisc@gmail.com", "license": "MIT",+ "dependencies": {
+ "my-monorepo-boilerplate-app-sidebar": "0.0.0"
+ },
"private": false }
Example 4-133. Create a packages/store/reducers.js file
import
{
combineReducers
}
from
'redux'
;
import
{
reducer
as
appSidebarReducer
,
storeKey
as
appSidebarStoreKey
}
from
'my-monorepo-boilerplate-app-sidebar'
;
export
default
combineReducers
({
[
appSidebarStoreKey
]
:
appSidebarReducer
,
});
Example 4-134. Create a packages/store/index.js file
import
{
createStore
,
applyMiddleware
,
compose
,
}
from
'redux'
;
import
{
middleware
as
appSidebarMiddleware
,
}
from
'my-monorepo-boilerplate-app-sidebar'
;
import
reducers
from
'./reducers'
;
export
default
(
initialstate
=
{})
=>
{
const
composeEnhancers
=
typeof
window
===
'object'
&&
window
.
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
?
window
.
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
({})
:
compose
;
return
createStore
(
reducers
,
initialstate
,
composeEnhancers
(
applyMiddleware
(
appSidebarMiddleware
,
),
),
);
};
Now that the store
is implemented, it needs to be added to the frontend
package and hooked up to the top level App
component (Examples 4-135 and 4-136).
Example 4-135. Add the store package as a dependency of the frontend package
{ "name": "my-monorepo-boilerplate-frontend", "version": "0.0.0", "description": "My Monorepo Boilerplate Frontend Package", "main": "index.jsx", "author": "hharnisc@gmail.com", "license": "MIT",+ "dependencies": {
+ "my-monorepo-boilerplate-store": "0.0.0"
+ },
"private": false }
Example 4-136. Update the packages/frontend/index.jsx file
import React from 'react' import { render } from 'react-dom'+import { Provider } from 'react-redux'
+import createStore from 'my-monorepo-boilerplate-store'
import App from './components/App'+// create the store and dispatch an APP_INIT action
+const store = createStore();
+store.dispatch({
+ type: 'APP_INIT',
+})
+
render(- <App />,
+ <Provider store={store}>
+ <App />
+ </Provider>,
document.getElementById('root'), )
Since the store
package uses the spread operator, we’ll need to add the babel transform and update the babel configuration (Examples 4-137 and 4-138). We’ll also need to use babel-polyfill to use async/await in the app-sidebar
package (Example 4-139).
Example 4-137. Install the Object Rest Spread babel transform
yarn add -DW babel-plugin-transform-object-rest-spread
Example 4-138. Update the .babelrc configuration
{ "presets": [ "env", "react" ], "plugins": [- "transform-export-extensions"
+ "transform-export-extensions",
+ "transform-object-rest-spread"
] }
Example 4-139. Update the packages/backend/webpack.config.js file
module.exports = {
context: __dirname,
entry: [
+ 'babel-polyfill',
'../frontend/index.jsx',
],
output: {
path: __dirname,
filename: 'bundle.js',
publicPath: '/static/',
},
When the application is loaded on the browser, the client will make a request to the backend endpoint /api/user that will fail because it hasn’t been implemented yet. Using Redux Devtools, it is possible to see all of the actions and the state at each step (Figure 4-8).
Let’s implement the backend endpoint to pass the client user data so the request succeeds (Example 4-140).
Example 4-140. Add a GET handler for the /api/user endpoint in packages/backend/index.js
const { readFileSync } = require('fs') const { join } = require('path') const express = require('express') const app = express() const webpack = require('webpack') const config = require('./webpack.config') const webpackMiddleware = require('webpack-dev-middleware') const compiler = webpack(config) app.use(webpackMiddleware(compiler, { publicPath: config.output.publicPath, })) const html = readFileSync(join(__dirname, 'index.html'), 'utf8')+app.get('/api/user', (req, res) => res.json({
+ profile: {
+ image: 'http://0.gravatar.com/avatar/cbb0e0bb8e68fe44de2dfde0e06',
+ name: 'Bobson Dugnutt',
+ }
+}))
app.get('*', (req, res) => res.send(html)) app.listen(3000)
Now when the browser makes a request to the backend, the USER_FETCH_SUCCESS action is triggered and the reducer stores the user in Redux (Figure 4-9).
Figure 4-10 shows what the application state looks like after the action is dispatched.
With the data being requested from the backend and stored in redux, we’re ready to add the AppSidebar
container to the frontend App
component (Examples 4-141 and 4-142).
Example 4-141. Add the app-sidebar package as a dependency of the frontend package
{
"name": "my-monorepo-boilerplate-frontend",
"version": "0.0.0",
"description": "My Monorepo Boilerplate Frontend Package",
"main": "index.jsx",
"author": "hharnisc@gmail.com",
"license": "MIT",
"dependencies": {
+ "my-monorepo-boilerplate-app-sidebar": "0.0.0",
"my-monorepo-boilerplate-store": "0.0.0"
},
"private": false
}
Example 4-142. Add the AppSidebar container to packages/frontend/components/App/index.jsx
import React from 'react'+import AppSidebar from 'my-monorepo-boilerplate-app-sidebar'
-export default () => <div>My Application</div>
+export default () =>
+ <div
+ style={{
+ display: 'flex',
+ height: '100%',
+ }}
+ >
+ <div
+ style={{
+ width: '20rem',
+ }}
+ >
+ <AppSidebar />
+ </div>
+ <div
+ style={{
+ padding: '2rem',
+ }}
+ >
+ <div>My Application</div>
+ </div>
+ </div>
If you’re following along with this code, you’ll now be seeing an AppSidebar
container rendered in the application, looking something like Figure 4-11.
Migrate the First Molecule
Looking back at “Migrate the First Atom”, we’ve already figured out how to bring Atoms into the existing Application. Let’s migrate the AppSidebar
Molecule into the existing application. For the purposes of this exercise, the my-monorepo-boilerplate-app-sidebar
Molecule will be used in the examples, but you’ll want to use your own Molecule when doing the migration.
First, we will publish new app packages to NPM. We need to add a script to Lerna to manage dependencies (Example 4-144).
Example 4-144. Add the publish script to use Lerna to manage dependencies to package.json
"scripts": { "start": "cd ./packages/backend/ && yarn start", "package-install": "yarn",- "start-storybook": "start-storybook -p 9000"
+ "start-storybook": "start-storybook -p 9000",
+ "publish": "lerna publish --exact"
},
To publish the packages you’ll need to be authenticated with NPM first and then use the publish
command (Example 4-145).
Example 4-145. Publish all the packages to NPM
# login to your NPM account with yarn
yarn login# follow prompt...
yarn run publish# in the root of the project
# you should see output that looks like this:
lerna info version 2.5.1 lerna info current version 0.0.0 lerna info Checkingfor
updated packages... lerna info Comparing with step-8. lerna info Checkingfor
prereleased packages... ? Select a new version(
currently 0.0.0)
Patch(
0.0.1)
Changes: - my-monorepo-boilerplate-app-sidebar: 0.0.0=
> 0.0.1 - my-monorepo-boilerplate-backend: 0.0.0=
> 0.0.1 - my-monorepo-boilerplate-frontend: 0.0.0=
> 0.0.1 - my-monorepo-boilerplate-store: 0.0.0=
> 0.0.1 ? Are you sure you want to publish the above changes? Yes lerna info publish Publishing packages to npm... lerna info published my-monorepo-boilerplate-backend lerna info published my-monorepo-boilerplate-app-sidebar lerna info published my-monorepo-boilerplate-store lerna info published my-monorepo-boilerplate-frontend lerna info git Pushing tags... Successfully published: - my-monorepo-boilerplate-app-sidebar@0.0.1 - my-monorepo-boilerplate-backend@0.0.1 - my-monorepo-boilerplate-frontend@0.0.1 - my-monorepo-boilerplate-store@0.0.1 lerna success publish finished
The Lerna publish Command
According to the Lerna publish
README file, the command does the following:
- Run the equivalent of
lerna updated
to determine which packages need to be published. - If necessary, increment the
version
key in lerna.json. - Update the package.json of all updated packages to their new versions.
- Update all dependencies of the updated packages with the new versions, specified with a caret (^) (unless the
--exact
command is specified). - Create a new Git commit and tag for the new version.
- Publish updated packages to npm.
Next, we will install Molecule dependencies (Example 4-146).
Example 4-146. Add the AppSidebar Molecule to the existing applicaton
yarn add my-monorepo-boilerplate-app-sidebar
Adding a mount point to the existing application is the next step. Depending on the starting point, a mount point might be added in the main HTML or a template (Example 4-147).
Example 4-147. Adding a mount point myNewAppSidebar to an index.html file.
<body>
<!-- Add a div to mount the new Text Atom -->
<div
id=
"myNewAppSidebar"
></div>
</body>
If the project’s starting point is another framework, like Backbone, be sure to render the mount point before trying to render the new Atom (Example 4-148).
Example 4-148. A Backbone view rendering the mount point
Backbone
.
View
.
extend
({
render
:
()
=>
{
const
html
=
`<div id="myNewAppSidebar"></div>`
;
this
.
setElement
(
$
(
html
)
);
return
this
;
}
});
Render the first Molecule
After creating a mount point for the new Molecule, it’s time to import the AppSidebar Molecule and render it along with the existing UI (Example 4-149).
It is important to note that the mount point must exist before React can render the new AppSidebar component. Also, if the mount point is re-rendered, the new AppSidebar component must be rendered again.
Example 4-149. A Backbone view responding to model changes and rendering a React component
import
Backbone
from
'backbone'
;
import
React
from
'react'
;
import
ReactDOM
from
'react-dom'
;
import
AppSidebar
from
'my-monorepo-boilerplate-app-sidebar/components/AppSidebar'
;
Backbone
.
View
.
extend
({
initialize
:
()
=>
{
// call render when model changes
this
.
model
.
on
(
'change'
,
this
.
render
,
this
);
},
render
:
()
=>
{
const
html
=
`<div id="myNewAppSidebar"></div>`
;
this
.
setElement
(
$
(
html
)
);
ReactDOM
.
render
(
React
.
createElement
(
AppSidebar
,
{
loading
:
this
.
model
.
get
(
'loading'
),
profile
:
this
.
model
.
get
(
'profile'
),
}),
document
.
getElementById
(
'myNewAppSidebar'
)
);
return
this
;
}
});
It is important to note that the file imports the stateless AppSidebar component rather than a container. This allows the AppSidebar component to be used with the existing Backbone model. In order to use the AppSidebar container, a Redux store would need to be added to the existing Application with the reducers and middleware tied into the store.
Continuing on the Migration Path
Congratulations, you’ve completed the first cycle of migration! The next steps will follow a similar format to what has already been migrated, but the foundation is in place. A typical migration cycle goes as follows:
- Choose a new feature (Molecule) to build or an existing feature to migrate
- Identify and breakdown Atoms that make up the feature (Molecule)
- Implement the Atoms that don’t exist
- Assemble the Atoms to implement the Molecule
- Build reducers, middleware, and actions to interact with the store
- Integrate the Molecule into a new Application
- Migrate the Molecule into the existing Application
- Go to step 1
Steps 1 through 6 are pretty much the same no matter which feature is being migrated. Step 7 often requires some extra thinking in order to keep the migration process moving smoothly.
There are two general approaches that provide a spectrum of options. On one extreme, you move slowly and make the existing application work more like the new application. A step forward with this approach would be adding a Redux store to the existing application. This would provide a similar developer experience and quality in both applications.
The other extreme is to move quickly and utilize as much of the existing application as possible. A step forward with the quick approach is to move on to the next feature and leave the current Molecules hooked up to the existing store. This would provide two different developer experiences and likely slightly reduce the quality of the existing application.
Both approaches can work well, but they each have their own set of trade-offs. When deciding the path forward consider the time frame, the team size, and the quality of service you’d like to provide to your existing customers. Whichever path you choose, we wish you the best of luck with your migration!
This chapter has outlined how technically to undertake a migration, and you should have a good practical understanding at this point of how you’ll migrate your own application. In the next section, we’ll show you how to build a compelling business case for rewriting your application and structure a team for undertaking the project successfully.
Get Atomic Migration Strategy for Web Teams now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.