Using yarn symlinks to share code between apps

If you're working on multiple apps and you want to share code between them - for example common utils, data types or UI components - there are several options available.

I was recently browsing a reddit thread about monorepos and sharing code between multiple apps and someone mentioned:

In my experience a monorepo with code symlinked at the src level is the best. The other options are a pain in the ass

And I wondered - what does "monorepo with code symlinked at the src level" even mean? And how would you go about setting up something like this?

A bit of browsing through the yarn docs and I stumbled on this piece:

yarn add link:/path/to/local/folder installs a symlink to a package that is on your local file system. This is useful to develop related packages in monorepo environments.

So then a monorepo setup using yarn symlinks would simply consist of several libraries and apps, stored as siblings in one main repository, which reference each other through yarn local dependencies.

This can be a great way to share code if:

  • you're looking for a simple solution to share code
  • you usually release your apps together (i.e. frontend + backend that share a library)

This might not be the best option if:

  • you want to release and publish each package / app independently
  • you want to be able to lock the shared package to a specific version, if needed

If you think this might be a good solution for you, read along for a step by step walkthrough of how to set this up.

Folder structure

For the sake of this example, we'll use two projects - project-a and project-b - and a library that I named very bluntly shared-code 😃

All the code is available on Gitlab, if you want to check it out yourself.

Our monorepo will simply have all of these as siblings at the root level:

- shared-code
  - package.json
  - yarn.lock
- project-a
  - package.json
  - yarn.lock
- project-b
  - package.json
  - yarn.lock

Notice how each app / library is completely independent and has its own package.json and yarn.lock file. This is good because it keeps each project simple and fully contained.

Adding dependencies

To make shared-code available in both project-a and project-b you can simply run yarn add link:/path for each:

cd project-a
yarn add link:../shared-code
cd ..
cd project-b
yarn add link:../shared-code

This will add shared-code as a dependency in the package.json file:

Local development flow

When something is changed in the shared-code library, does it get automatically refreshed or do you need to always rebuild the package?

The short answer is - it depends.

If you use Create React App, this won't work. First, the app expects the shared-code library to already be built, and second, the default CRA Webpack config does not detect changes in the shared-code, as it's outside of its src folder.

If you use your own custom setup, you can ensure hot reload by customizing your webpack config to detect changes in the symlinked packages. This is where having all the code under src might come in handy.

Continuous integration

How would this work in CI?

Since all the code is in the same repository, the library will always be available to yarn on yarn install. However, yarn assumes the shared-code is already built, so will need to manually do this step when building any of the projects:

# Sample CI build script
# 1. First build the `shared-code`
cd shared-code
yarn install
yarn build
cd ..
# 2. Build the project
yarn install # Will symlink `shared-code` to the current `node_modules`
yarn build

That's it!

Caveats

This might make for a simple way to setup code sharing and a seamless developer experience, but there are several downsides to be aware of.

The most important one is related to the fact that both project-a and project-b always use just one version of the shared-code - the latest one available at a given point in time.

Since the shared-code is not published to an npm repository it's not possible to lock in a certain version. This means that there's a high chance that a change needed for project-a might break project-b unexpectedly.

Where to go from here

While this setup is good to get started, with time you might "grow" out of it.

If you notice a lot of your dependencies are duplicated in each of the apps / packages, and you'd like to hoist them to the root level, you might want to look into using Yarn Workspaces.

If you decide you want to publish the packages to an npm repository and want better versioning and publishing support, you might want to look at Lerna.

I hope you found this useful! Let me know in the comments below if you have any questions or thoughts!