I recently <a href="//labs.mlssoccer.com/the-road-to-universal-components-at-major-league-soccer-eeb7aac27e6c" target="_blank">wrote about</a> why we chose universal components at Major League Soccer and I received a lot of feedback asking about the specifics of how we actually implemented our UCs system.
Companies Mentioned
Coin Mentioned
I recently why we chose universal components at Major League Soccer and I received a lot of feedback asking about the specifics of how we actually implemented our UCs system.
While it doesn’t make sense for us to open source our MLS specific components, I didn’t want to leave everyone hanging on exactly how we implement UCs so I created an and will walk through setting up your own UC system in this post.
The deployed storybook can be found .
Choosing A Solution
The very first challenge you will face when implementing UCs is what solution to base your library on. Currently there are two choices for this, [react-native-web](//github.com/necolas/react-native-web) (RNW) and [react-primitives](//github.com/lelandrichardson/react-primitives) (RP). At Major League Soccer we decided to go with RNW for two reasons:
It’s more mature than react-primitives. This might sound odd since both are technically in early alpha, but RNW currently has better parity with React Native and supports React 16. ()This means that RNW will work out of the box with both create-react-app ,create-react-native-app, and react-native init. Examples of both CRA and CRNA can be found in the at the beginning of this article.
RP uses the Touchable primitive which doesn’t technically map directly to RN. There is a Touchable api in React Native but it’s not a component like TouchableOpacity or any of the other React Native touchables. This requires more complex setup.
Setting Up the Project
We are going to use to setup a monorepo. This will make managing the deployment of our universal components package (and any packages we may create in the future) easier.
First we need Lerna installed.
yarn add --global lerna
Then we need to create a new folder for our project, and from the root of the project run yarn init to create a new package.json and then lerna init to set up Lerna.
This will create a bare bones setup. You should have a project that looks like this:
packages/lerna.jsonpackage.json
Let’s add a .gitignore at the root and make sure to ignore all node_modules directories we may end up with.
**/node_modules/**
Now we should have a project structure like this:
packages/.gitignorelerna.jsonpackage.json
Next we need to create our universal-components package. Inside the packages directory create a new folder called universal-components. Then cd into that directory and run yarn init again (for this package use the name you want to import components from in your apps).
packages/universal-components/package.jsonlerna.jsonpackage.jsonNow that we have our package ready, we need to setup for universal components. This means we need support for transpiling code for testing/storybook, as well as aliasing for RNW.
First let’s install all needed babel dependencies:
// from within packages/universal-components
yarn add -D babel-plugin-module-resolver babel-plugin-transform-class-properties babel-plugin-transform-es2015-modules-commonjs babel-preset-flow babel-preset-react babel-preset-stage-2 flow-bin
That’s a lot of modules! Let’s see what each does:
babel-plugin-module-resolver: used for aliasing and adding .web.js extension support
babel-plugin-transform-class-properties: used to add support for class properties which are supported in React Native.
babel-plugin-transform-es2015-modules-commonjs: used to add support for import/export statements without needing .default
babel-preset-flow: used to add Flow support
babel-preset-react: used to add React support
babel-preset-stage-2: used to add things like async/await and rest/spread operators (supported by React Native)
flow-bin: used to run Flow against our components
Next we need to create a .babelrc file in our new universal-components package and then add the following config:
{"plugins": ["transform-class-properties","transform-es2015-modules-commonjs",["module-resolver",{"alias": {"react-native": "react-native-web"},"extensions": ["web.js", ".js"]}]],"presets": ["react", "stage-2", "flow"]}Now that we can support universal components we need to install React:
yarn add -D react react-dom react-native-web prop-types
We add them as devDependencies because we don’t want them to install with our universal components. In a native environment we don’t want to install react-native-web and vice versa. So instead we add them as devDepenencies and also include them in the peerDepencencies section of our package.json file. This way when users of the UC package install, they will be warned about needed unmet peer dependencies.
We don’t add react-native as a devDependency because we are using the web version of Storybook and don’t need React Native in this environment.
The last thing we need to do is create a components directory that will be the future home to our universal components. 🏠
Once that is done your project should look like the following:
packages/universal-components/components.babelrcpackage.jsonyarn.locklerna.jsonpackage.jsonAlright! Now we’re ready to develop our first universal component! 🎉
Setting Up Storybook
In order to ensure we keep an organized development process (and to ensure our components work properly on web), we will use to isolate development and get realtime feedback about how your components look and behave.
First thing we have to do is add storybook as a dependency to our universal-components package.
// from within packages/universal-components
yarn add -D @storybook/react
Next we need to create a .storybook directory for our Storybook config:
Inside of .storybook add a config.js file. This is where our basic Storybook configuration goes. Inside of config.js add the following:
import { configure } from '@storybook/react';
const req = require.context('../components/', // path where stories livetrue, // recursive?/\__stories__\/.*.js$/, // story files match this pattern);
function loadStories() {req.keys().forEach(module => req(module));}configure(loadStories, module);
The important takeaway is we configure Storybook to look in our components directory for __stories__ directories and load them.
This lets us keep our stories next to the related component which is nice and matches up nicely with default configuration (more on testing in a bit).
Next we have to alter the default Webpack config for Storybook so that we can properly parse our imported universal components. Inside the .storybook directory create a webpack.config.js file and add the following:
const path = require('path');const webpack = require('webpack');// use babel-minify due to UglifyJS errors from modern JS syntaxconst MinifyPlugin = require('babel-minify-webpack-plugin');
// get default webpack config for @storybook/reactconst genDefaultConfig = require('@storybook/react/dist/server/config/defaults/webpack.config.js');
Now we won’t experience any issues during our production builds of Storybook and are ready to create a component, but first let’s add a few script entries in package.json to make our lives a bit easier.
Let’s create a new directory in components for a Button component:
components/button/__stories__/index.jsindex.js
Inside of button/index.js add the following code:
// @flow
import React, { Component } from 'react';import {Platform,StyleSheet,Text,TouchableOpacity,View,} from 'react-native';import PropTypes from 'prop-types';
With a story set up, we can now run storybook and see the component in action. From the universal-components directory run yarn storybook. That’s it. You now have a development environment up and running. 🐎
Universal Components Storybook
Setting Up Testing
Building components in Storybook definitely reduces the chance of error but if you want to test functionality programmatically you will need to set up testing. For testing universal components you can use and which are already largely used in the React community.
First thing we have to do is add our testing dependencies:
// from universal-components directory
yarn add -D enzyme enzyme-to-json jest
Now that we have our testing dependencies we need to set up some tests. In the button directory in components add a new directory called __tests__ with an index.js file.
Now we need to add a script entry in package.json so we can run our tests easily.
// in universal-components/package.json
"scripts": {"test": "jest",...},
With a few tests in place and our package.json updated, we can now run yarn test to ensure everything is working.
Jest Tests
Deploying
The last step is deploying both Storybook and our universal components library. Let’s start with storybook.
We already added a script for building Storybook for production ("build": "build-storybook"), but we still need a way to deploy our Storybook so that users of our universal components library know what is available to them. To accomplish this we’ll use [surge.sh](//surge.sh/).
First we need to add surge as a new devDependency:
// from universal-components directory
yarn add -D surge
Don’t forget to add packages/universal-components/storybook-static to your .gitignore!
And last but not least we need to publish our universal components package. However, before we can we should configure an .npmignore in our universal-components packages so we don’t deploy any unnecessary files.
node_modules.storybook.babelrc
Earlier I mentioned that we’re using Lerna to make publishing packages easier. To publish a new version of the package run lerna publish from the root directory of the project (not theuniversal-components directory).
That’s it! You now have a full universal component workflow up and running! Give yourself a pat on the back, or a virtual high-five! ✋
If you have any questions about this universal components implementation feel free to comment below or reach out to me on , my DMs are always open!