Home

Awesome

How to use yarn workspaces with Create React App and Create React Native App (Expo) to share common code across

This post is on medium too!

The goal of this tutorial is to make a monorepo using yarn workspaces to share common code across a Create React App (CRA) and a Create React Native App (CRNA/Expo).

There are currently some issues with the projects that when fixed, these workarounds shouldn't be needed anymore:

Some of the solutions below may also help for lerna setups.

Pre-requisites

Make sure you're running node ~ version 8 and at least yarn 1.3.0 and have create-react-app and create-react-native-app installed.

Setup workspaces

In this guide, we'll setup four folders but feel free to structure it as you see fit:

Make a new folder where you want your workspaces to be and add a package.json that looks like this:

{
  "private": true,
  "workspaces": [
    "web",
    "native",
    "core",
    "views"
  ]
}

For the rest of this guide, we're going to assume that this folder is called workspaces and it's in your home directory. We will refer to it as ~/workspaces.

Setup core

core in our example will be just an empty project. Make a core folder and put this package.json inside:

{
  "name": "core",
  "version": "0.0.1"
}

Let's put a few sample files in there to use as a test. We'll also leverage the project specific extensions in web and native.

test.js:

import value from './value'
export default value

value.native.js:

export default 'value in native'

value.web.js:

export default 'value in web'

Setup views

We will use Views for our UI. If you want to use React directly, you may still benefit from this folder by putting shared components across your projects here. Otherwise, just skip this section.

views is where our UI sits. Make a views folder and put this package.json inside:

{
  "name": "views",
  "version": "0.0.1",
  "scripts": {
    "native": "views-morph . --as react-native --watch",
    "native:build": "views-morph . --as react-native",
    "web": "views-morph . --as react-dom --watch",
    "web:build": "views-morph . --as react-dom"
  }
}

Then add the latest views-morph to it:

yarn add --dev views-morph

Add a file called Test.view with this:

Test Vertical
backgroundColor deepskyblue
margin 50
onClick props.onClick
Text
fontSize 28
text Hey I'm a button!

Views uses some CSS defaults that make it behave close to how React Native renders the UI, add them by copying views.css to src/index.css.

Views is a productive way to create interfaces together with your design team and design in production. If you want to learn more about it, reach out at https://twitter.com/viewsdx or join the conversation at https://slack.viewsdx.com :).

Web

There are some issues with running CRA's init scripts inside the workspace, so just go to a temporary folder anywhere and make a new project:

# go to some temporary location
cd /tmp
# make the app
create-react-app web
# get rid of node modules and yarn.lock
rm -rf web/node_modules web/yarn.lock
# move it to the workspaces
mv web ~/workspaces
cd ~/workspaces/web

The next step is to have CRA compile your other workspaces code if they're imported by your app.

Install react-app-rewired and react-app-rewire-yarn-workspaces in the web project:

yarn add --dev react-app-rewired react-app-rewire-yarn-workspaces

Swap the start, build, and test scripts in package.json for these:

    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test --env=jsdom",

And add a file called config-overrides.js with this:

const rewireYarnWorkspaces = require('react-app-rewire-yarn-workspaces');

module.exports = function override(config, env) {
  return rewireYarnWorkspaces(config, env);
};

To test the connection with core, add this to src/App.js:

import test from 'core/test'

alert(test)

If you're using Views, test it by overwriting App.js with this:

import React, {Component} from 'react';
import test from 'core/test';
import Test from 'views/Test.view.js';

alert(test);

class App extends Component {
  render() {
    return <Test onClick={() => alert('just clicked a button!')} />;
  }
}

export default App;

Native

There are some issues with running CRNA's init scripts inside the workspace, so just go to a temporary folder anywhere and make a new project:

# go to some temporary location
cd /tmp
# make the app
create-react-native-app native
# get rid of node modules and yarn.lock
rm -rf native/node_modules native/yarn.lock
# move it to the workspaces
mv native ~/workspaces
cd ~/workspaces/native

We'll first need to swap CRNA's entry point because the way it picks up our App.js is very much dependent on the location of files, so it's easier this way. We'll call that file crna-entry.js.

Either get the original file from here. If you do, make sure you change import App from '../../../../App'; for import App from './App'; so it picks up your app.

...or, use this version want to avoid wrapping your app in a View. Add a file called crna-entry.js with this:

import App from './App';
import Expo from 'expo';
import React from 'react';

const AwakeInDevApp = props => [
  <App key="app" {...props} />,
  process.env.NODE_ENV === 'development' ? (
    <Expo.KeepAwake key="keep-awake" />
  ) : null,
];
Expo.registerRootComponent(AwakeInDevApp);

After that, in package.json, replace:

  "main": "./node_modules/react-native-scripts/build/bin/crna-entry.js",

for:

  "main": "crna-entry.js",

Then, replace app.json for this:

{
  "expo": {
    "sdkVersion": "23.0.0",
    "ignoreNodeModulesValidation": true,
    "packagerOpts": {
      "config": "rn-cli.config.js",
      "projectRoots": ""
    }
  }
}

Note that this guide was created when Expo's SDK was at v23.0.0. If your app.json has a different version, use that instead.

Install metro-bundler-config-yarn-workspaces and crna-make-symlinks-for-yarn-workspaces:

yarn add --dev metro-bundler-config-yarn-workspaces crna-make-symlinks-for-yarn-workspaces

Add a file called rn-cli.config.js with this:

const getConfig = require('metro-bundler-config-yarn-workspaces')
module.exports = getConfig(__dirname)

If your workspaces are not located in the root folder (e.g. root/packages/*) you must provide a nodeModules option indicating where the node_modules root folder is located as described below:

import test from 'core/test'

const getConfig = require('metro-bundler-config-yarn-workspaces')

const options = { nodeModules: path.resolve(__dirname, '..', '..') }

module.exports = getConfig(__dirname, options)  

Add a file called link-workspaces.js with this:

require('crna-make-symlinks-for-yarn-workspaces')(__dirname)

Add prestart script to your native project's package.json:

    "prestart": "node link-workspaces.js",

To test the connection with core, add this to App.js:

import test from 'core/test'

alert(test)

If you're using Views, test it by overwriting App.js with this:

import React, {Component} from 'react';
import test from 'core/test';
import Test from 'views/Test.view.js';

alert(test);

class App extends Component {
  render() {
    return <Test onClick={() => alert('just clicked a button!')} />;
  }
}

export default App;

If you get an error like Cannot find entry file crna-entry.js in any of the roots..., press shift+R when you start the expo runner so it restarts the packager and clears the cache.

Part of the setup may also come in handy for React Native CLI. See this comment. I also wanted to thank Neil Ding @GingerBear for his gist, without it metro-bundler-config-yarn-workspaces wouldn't be possible.

Before starting the apps...

At this point, I'd probably recommend wiping all the node_modules of each project and starting from scratch:

cd ~/workspaces
rm -rf node_modules core/node_modules views/node_modules native/node_modules web/node_modules
yarn

Dependencies are still added to the different project folders.

If you're using Views, you need to start the morpher by project type until viewsdx/morph#31 is implemented.

For web, in the views folder, run:

yarn web

For native, in the views folder, run:

yarn native

We'll be providing a concurrent process runner like the one implemented in https://github.com/viewsdx/use soon.

I hope the process works for you! This is the GitHub repo that contains a sample project and the supporting dev packages used in here. If you find any issues or have suggestions around some of the steps, feel free to open an issue.

Thanks to Larissa and Neil for their help 🙏.

Happy hacking!