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. wrote about 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. example repository The deployed storybook can be found . here 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, (RNW) and (RP). At Major League Soccer we decided to go with RNW for two reasons: [react-native-web](https://github.com/necolas/react-native-web) [react-primitives](https://github.com/lelandrichardson/react-primitives) 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 , , and . Examples of both CRA and CRNA can be found in the at the beginning of this article. https://github.com/airbnb/react-sketchapp/issues/104 create-react-app create-react-native-app react-native init example repository I mentioned RP uses the primitive which doesn’t technically map directly to RN. There is a api in React Native but it’s not a component like or any of the other React Native touchables. This requires more complex setup. Touchable Touchable TouchableOpacity 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. Lerna 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 to create a new and then to set up Lerna. yarn init package.json lerna init This will create a bare bones setup. You should have a project that looks like this: packages/lerna.jsonpackage.json Let’s add a at the root and make sure to ignore all directories we may end up with. .gitignore node_modules **/node_modules/** Now we should have a project structure like this: packages/.gitignorelerna.jsonpackage.json Next we need to create our package. Inside the directory create a new folder called . Then into that directory and run again (for this package use the name you want to import components from in your apps). universal-components packages universal-components cd yarn init packages/universal-components/package.jsonlerna.jsonpackage.json Now 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. Babel First let’s install all needed dependencies: babel // 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: : used for aliasing and adding extension support babel-plugin-module-resolver .web.js used to add support for class properties which are supported in React Native. babel-plugin-transform-class-properties: used to add support for import/export statements without needing babel-plugin-transform-es2015-modules-commonjs: .default used to add Flow support babel-preset-flow: used to add React support babel-preset-react: : used to add things like and operators (supported by React Native) babel-preset-stage-2 async/await rest/spread : used to run Flow against our components flow-bin Next we need to create a file in our new package and then add the following config: .babelrc universal-components {"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 because we don’t want them to install with our universal components. In a native environment we don’t want to install and vice versa. So instead we add them as and also include them in the section of our file. This way when users of the UC package install, they will be warned about needed unmet peer dependencies. devDependencies react-native-web devDepenencies peerDepencencies package.json We don’t add as a because we are using the web version of Storybook and don’t need React Native in this environment. react-native devDependency // in packages/universal-components/package.json "peerDependencies": {"prop-types": ">=15","react": ">=15","react-dom": ">=15","react-native": ">=0.42","react-native-web": ">=0.0.129"} The last thing we need to do is create a directory that will be the future home to our universal components. 🏠 components Once that is done your project should look like the following: packages/universal-components/components.babelrcpackage.jsonyarn.locklerna.jsonpackage.json Alright! 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. Storybook First thing we have to do is add storybook as a dependency to our package. universal-components // from within packages/universal-components yarn add -D @storybook/react Next we need to create a directory for our Storybook config: .storybook packages/universal-components/.storybook/components/.babelrcpackage.jsonyarn.locklerna.jsonpackage.json Inside of add a file. This is where our basic Storybook configuration goes. Inside of add the following: .storybook config.js config.js 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 directory for directories and load them. components __stories__ 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). Jest Next we have to alter the default Webpack config for Storybook so that we can properly parse our imported universal components. Inside the directory create a file and add the following: .storybook webpack.config.js 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'); const DEV = process.env.NODE_ENV !== 'production';const prodPlugins = [new webpack.DefinePlugin({'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),'process.env.__REACT_NATIVE_DEBUG_ENABLED__': DEV,}),new webpack.optimize.OccurrenceOrderPlugin(),new MinifyPlugin(),]; module.exports = (baseConfig, env) => {const config = genDefaultConfig(baseConfig, env);const defaultPlugins = config.plugins;const overwrite = {devtool: 'inline-source-map',module: {rules: [{test: /\.js$/,exclude: /node_modules/,loader: 'babel-loader',query: { cacheDirectory: true },},],},plugins: DEV ? defaultPlugins : prodPlugins,}; return Object.assign(config, overwrite);}; 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 entries in to make our lives a bit easier. script package.json // in universal-components/package.json "scripts": {"storybook": "start-storybook -p 9001 -c .storybook","build: "build-storybook"...}, Let’s create a new directory in components for a component: Button components/button/__stories__/index.jsindex.js Inside of add the following code: button/index.js // @flow import React, { Component } from 'react';import {Platform,StyleSheet,Text,TouchableOpacity,View,} from 'react-native';import PropTypes from 'prop-types'; export type ButtonProps = {backgroundColor: string,fontColor: string,onPress: () => void,size: string,style: StyleSheet.Styles,text: string,}; const getButtonPadding = (size: string): number => {switch (size) {case 'small':return 10;case 'medium':return 14;case 'large':return 18;default:return 14;}}; const getButtonFontSize = (size: string): number => {switch (size) {case 'small':return 10;case 'medium':return 16;case 'large':return 20;default:return 16;}}; export default class Button extends Component<ButtonProps, *> {static propTypes = {backgroundColor: PropTypes.string,fontColor: PropTypes.string,onPress: PropTypes.func.isRequired,size: PropTypes.string,style: PropTypes.oneOfType([PropTypes.array,PropTypes.object,PropTypes.string,]),text: PropTypes.string.isRequired,}; render = (): React$Element<*> => {const {backgroundColor = 'black',fontColor = 'white',onPress,size = 'medium',style,text,} = this.props;const computedStyles = styles(backgroundColor, fontColor, size); return ( <TouchableOpacity onPress={onPress}> <View style={\[computedStyles.container, style\]}> <Text style={computedStyles.text}> {text.toUpperCase()} </Text> </View> </TouchableOpacity> ); };} const styles = (backgroundColor: string,fontColor: string,size: string,): StyleSheet.styles =>StyleSheet.create({container: {backgroundColor: backgroundColor,borderRadius: 3,padding: getButtonPadding(size),},text: {backgroundColor: 'transparent',color: fontColor,fontFamily: Platform.OS === 'web' ? 'sans-serif' : undefined,fontSize: getButtonFontSize(size),fontWeight: 'bold',textAlign: 'center',},}); Now that we have a button component, let’s add a story so we can see it rendered. In add the following: button/__stories__/index.js import React from 'react';import { storiesOf } from '@storybook/react';import { View, Text, StyleSheet } from 'react-native'; import Button from '../'; storiesOf('Universal Components', module).add('Button', () => {return (<View style={styles.container}><Text style={styles.title}>Button</Text><View style={styles.example}><Text style={styles.exampleTitle}>Example</Text><View style={styles.exampleWrapper}><Buttontext="Press Me!"onPress={() => alert('Button Pressed!')}/></View></View></View>);}); const styles = StyleSheet.create({container: {padding: 32,},example: {borderColor: '#dddddd',borderWidth: 1,display: 'inline-flex',flex: 0,padding: 16,},exampleTitle: {fontFamily: 'sans-serif',fontSize: 18,fontWeight: 'bold',marginBottom: 12,},exampleWrapper: {width: 300,},title: {fontFamily: 'sans-serif',fontSize: 24,fontWeight: 'bold',marginBottom: 24,},}); With a story set up, we can now run storybook and see the component in action. From the directory run . That’s it. You now have a development environment up and running. 🐎 universal-components yarn storybook 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. Jest Enzyme 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 directory in add a new directory called with an file. button components __tests__ index.js components/button/__stories__/index.js__tests__/index.jsindex.js Add the following to : __tests__/index.js import React from 'react';import { mount } from 'enzyme';import { mountToJson } from 'enzyme-to-json'; import Button from '../'; describe('<Button />', () => {it('<Button text="Test" />', () => {const wrapper = mount(<Button text="Test" onPress={() => {}} />);expect(mountToJson(wrapper)).toMatchSnapshot();}); it('onPress()', () => {const spy = jest.fn();const wrapper = mount(<Button text="Test" onPress={spy} />); wrapper .find('TouchableOpacity') .first() .props() .onPress(); expect(spy).toBeCalled(); });}); Now we need to add a entry in so we can run our tests easily. script package.json // in universal-components/package.json "scripts": {"test": "jest",...}, With a few tests in place and our updated, we can now run to ensure everything is working. package.json yarn test Jest Tests Deploying The last step is deploying both Storybook and our universal components library. Let’s start with storybook. We already added a for building Storybook for production ( ), 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 . script "build": "build-storybook" [surge.sh](http://surge.sh/) First we need to add as a new : surge devDependency // from universal-components directory yarn add -D surge Next we need to add a : script "scripts": {"deploy": "yarn build && surge ./storybook-static",...} Don’t forget to add to your ! packages/universal-components/storybook-static .gitignore And last but not least we need to publish our universal components package. However, before we can we should configure an in our packages so we don’t deploy any unnecessary files. .npmignore universal-components 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 from the root directory of the project (not the directory). lerna publish universal-components 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! Twitter