Sharing source code and libraries in React

How to use shared libraries of components and tools across React projects with create-react-app (CRA)

Doron Oded
Capriza Engineering

--

Although React inherently glorifies componentization, it seems like a major aspect of componentization was overlooked, especially the aspect of sharing code between projects and apps. In large-scale projects, it would usually be smarter to split the project into smaller sub-projects and share as much as possible components and code between them. Unfortunately, it turns out that it’s a bit challenging to accomplish in React.

The Dilemma

Several questions arise when thinking about the architecture of multiple projects that use shared libraries:

  • Structure — Should the projects and shared libraries reside in a monorepo or in separate repositories?
  • Versioning — Should shared libraries have a different versioning lifecycle that is separate from the project versioning?
  • Reference — How should a project reference a shared library? Natively as an npm package using npm install or using import of a relative file system path?
  • Development — Is the development cycle of the shared library different from the development cycles of projects in terms of dev → test → deploy?

Answers to these questions are very important and will eventually converge to your shared library and project’s architecture and methodology.

At Capriza we chose to manage the projects and shared libraries as stand-alone repositories, where the shared libraries are published to a private npm registry and consumed by projects using npm install.

Setting up shared libraries

Let’s deep dive into creating and consuming a shared library. There are two main options here:

Compiled Code — Public React shared libraries (such as ant.design, recharts and many more) are packages with fully compiled code that other projects can import.

Uncompiled Code —Uncompiled code can be imported into the project and included in the build of the project itself..

A major advantage of importing uncompiled code is the ability to develop the shared library in parallel to the project itself without having to go through all the develop-compile-publish cycle of the shared library.

Consuming uncompiled code

In order to consume uncompiled code from an external library we have to tweak and adjust the webpack configuration of the project. But before I show you how, some background is necessary.

Changing the webpack configuration of a react project is not trivial, and many developers would prefer to avoid it as much as possible. That is exactly why the create-react-app (CRA) project is so popular — all the webpack configuration of the JSX compilation, babel transpilation and more is pre-configured for you in the ‘react-scripts’ package dependency. CRA also gives you the option to configure everything yourself using ‘npm eject’, which copies the webpack configuration files to your root folder where you can edit it manually. Just remember that once ejected, you are on your own; you cannot undo the ejection.

A popular solution for configuring webpack without ejecting is through the use of react-app-rewired that replaces the ‘npm start’ and ‘npm build’ scripts with its own scripts. Note that react-app-rewired actually calls the original ‘react-scripts’, but then lets you override the outputted configuration. Version 2.0 of CRA caused parts of ‘react-app-rewired’ to break, so another package was created to bridge the gap — customize-cra.

Step by step example

Now that we have the basic background out of the way, let’s see how to create a shared library of components and tools that can be imported and compiled by a different project.

Step 1 — Create two react apps

Create two sibling React apps using `npx create-react-app` — one for the shared library called shared-lib and one for the consuming project called simply project.

2 create-react-apps: (1) project; (2) shared-lib

Step 2 — Create the shared component

Create a simple shared component in the shared-lib src folder.
Run npm start to see it live.

SharedComp.js in progress
Our shared-lib app, with our shared component — SharedComp.js

Step 3 — Install the shared component

Now, let’s have project install our shared-lib as a local npm package by running npm install "../shared-lib". Now the node_modules of project contains shared-lib as a symlink:

shared-lib in the project’s node_modules as a symlink

And, the package.json of project looks like:

project’s package.json with the local shared-lib reference

Step 4 — Import the shared component

Naively trying to import our SharedComp in our project’s app.js results in an Unexpected token syntax error.

Importing SharedComp in the project’s App.js

The reason for this error is that we are trying to import a module from our node_modules which is expected to already be compiled. Uncompiled JSX in our case is not supported.

Step 5 — Install react-app-rewired and customize-cra

To overcome the Failed to compile error we need to change the default webpack configuration so that the shared-lib files will be compiled in addition to the files in the src folder. This requires installing the react-app-rewired and customize-cra packages that will enable over-riding webpack configs without ejecting in our main project.

npm install react-app-rewired customize-cra --save-dev

We create a config-overrides.js file in our root project folder that looks like this:

var path = require ('path');
var fs = require ('fs');
const {
override,
addDecoratorsLegacy,
babelInclude,
disableEsLint,
} = require("customize-cra");

module.exports = function (config, env) {
return Object.assign(config, override(
disableEsLint(),
addDecoratorsLegacy(),
/*Make sure Babel compiles the stuff in the common folder*/
babelInclude([
path.resolve('src'), // don't forget this
path.resolve('node_modules/shared-lib/src')
])
)(config, env)
)
}

The config-overrides.js file exports a function that receives the configuration and environment as parameters and outputs the new overridden configuration. The override function receives functions as arguments and runs each of those on the same config that is passed from one to the other. The first two functions ( disableEsLint, and addDecoratorsLegacy) are required if you are working with mobx.

Step 5 ½ — One final tweak

There is one last thing to do before we can run our project. The locally referenced shared-lib has been installed in project using a symlink. For some reason symlinks don’t work as expected, but fortunately Mike Fisher suggested an incredibly simple trick — change path.resolve("node_modules/shared-lib/src") to fs.realpathSync("node_modules/shared-lib/src") and you’re all set.

It works!

Now run npm start in the project and voila! Our project runs with a shared component installed via `npm install`.

From here, there are a lot of other configurations and overrides you can use, just visit react-app-rewired and customize-cra for more details about these great tools.

Final note

When I started working on this shared-lib project at Capriza, my main react project had been originally built with CRA ver 1.x.
It took me a lot of time and effort to realize that the best way to upgrade the project to CRA 2.x and implement the use of shared libs was to create a side project with CRA 2.x, install all the relevant dependencies and finally copy the source files from my old project to the new CRA 2.x project instead of upgrading the original project, package by package, as I did. If you want to preserve the Git history of the original project, just copy the finalized package.json of the working project to the old one, remove the node_modules and run npm install. Worked for me.

The full shared library example is available here: https://github.com/doronoded/shared-lib-example

You are more than welcome to comment and/or describe any problems you encounter while trying to do the same. I’ll do my best to help out as much as I can.

--

--

Doron Oded
Capriza Engineering

I am a Fullstack Team Leader at AU10TIX. I am working in web development since 2012. Started my own Startup, have an M.SC degree in Info. Sys. Eng.