O'Reilly logo

Atomic Migration Strategy for Web Teams by Katie Womersley, Harrison Harnisch

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

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:

  1. Generate a static JavaScript bundle of client-side code.
  2. Modify the server to serve the static JavaScript bundle.
  3. Inject a script tag in the head to load the client-side JavaScript bundle.
  4. (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.

Figure 4-1. An integrated organization pattern, where features can simply import any file from another part of the application or use third-party packages.

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.

Figure 4-2. Modular organization pattern: It’s now possible to refactor just one package

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.

Figure 4-3. Multidirectional data flow: state can be passed from any point in the system to any other point

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.

Figure 4-4. Central Store package: all state is managed via the Store package

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).

Figure 4-5. React Storybook
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:

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 packages
echo "# 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 backend
cd backend
yarn init
cd ../.. 
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 frontend
cd frontend
yarn init
cd ../..

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).

Table 4-1. A decision matrix for choosing the first Molecule to migrate
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.

Figure 4-6. Sidebar design

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.

Figure 4-7. Sidebar design broken down into 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'}
    >
      Google Search - Monorepo
    </Link>
  ))
  .add('unstyled', () => (
    <Link
      href={'https://www.google.com/search?q=css'}
      unstyled
    >
      (unstyled) Google 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-sidebar
cd 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/AppSidebar
cd 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 store
cd store
yarn init
cd ../..
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).

Figure 4-8. The FETCH_USER_FAIL action being dispatched

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-9. The FETCH_USER_SUCCESS action being dispatched

Figure 4-10 shows what the application state looks like after the action is dispatched.

Figure 4-10. The Redux state after the FETCH_USER_SUCCESS was triggered

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.

Figure 4-11. The AppSidebar container rendered in the application

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 Checking for updated packages...
lerna info Comparing with step-8.
lerna info Checking for 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:

  1. Run the equivalent of lerna updated to determine which packages need to be published.
  2. If necessary, increment the version key in lerna.json.
  3. Update the package.json of all updated packages to their new versions.
  4. Update all dependencies of the updated packages with the new versions, specified with a caret (^) (unless the --exact command is specified).
  5. Create a new Git commit and tag for the new version.
  6. 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:

  1. Choose a new feature (Molecule) to build or an existing feature to migrate
  2. Identify and breakdown Atoms that make up the feature (Molecule)
  3. Implement the Atoms that don’t exist
  4. Assemble the Atoms to implement the Molecule
  5. Build reducers, middleware, and actions to interact with the store
  6. Integrate the Molecule into a new Application
  7. Migrate the Molecule into the existing Application
  8. 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.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required