Creating a Monorepo with Turborepo for React Native and Next JS

How to create a monorepo with Turborepo with React Native and Next JS, and some limitations to watch out for with these tech stacks.

The image of Tony KoTony Ko
Apr 4, 2023
·
3 min read

Previously at Quantum Mob, I’ve covered what is a monorepo, the advantages of using them, widespread use cases of it, and why I use build tools such as Turborepo when managing a monorepo.

To quickly recap, a popular monorepo use case is hosting cross-platform applications and teams using the same backend and logic. It’s an effective tool for sharing code and avoiding duplication and silos between teams. Particularly, Turborepo greatly advances this idea as a great task runner between packages and by providing dead simple configuration.

In this blog post, I’m going to cover how to create a monorepo with Turborepo with React Native and Next JS, and some limitations to watch out for with these tech stacks. I’ll demonstrate how to run a dev environment and share packages such as utilities, types, and more between the applications. As such, this is an intermediate topic that expects a working understanding of React Native and web applications. By covering these topics, I hope to arm us against weeks of trial and error and come out on top with a solid monorepo boilerplate.

How will we achieve this?

To create a monorepo suitable for React Native + Web, we must do the following:

  1. Install global dependencies

  2. Consider React Native and Next JS limitations

  3. Create and configure Yarn 3 monorepo

  4. Add and configure React Native + web apps

  5. Add shared packages

  6. Set up Turborepo and build scripts

  7. Run dev environments and build for production

As a bonus, I’ve added a completed example of these steps so we can follow along. https://github.com/tkodev/ex-yarn-turborepo-demo

1. Install Global Dependencies

Let’s make sure Node and Yarn are installed.

  • Assuming we’re using a Mac for this tutorial, as we’ll be using bash and Xcode commands to manage files, run tasks, build for iOS, etc.

  • Install React Native requirements

    • Tools required to build react-native, such as Xcode, XCode CLI tools, Cocoapods, watchman, flipper, etc.

  • Install Node 18 (via node version manager like nvm or fmn if desired)

  • Re-open the terminal and test installation success via node -v, and yarn -v

2. Consider React Native and Next JS Limitations

This is my proposed directory structure for monorepos with React Native and Next JS 13.

./ex-yarn-turborepo-demo/ ├── apps/ │ ├── react-native/ │ │ ├── node-modules │ │ └── package.json │ └── next-js/ │ ├── node-modules │ └── package.json ├── packages/ │ └── utils/ │ ├── node-modules │ └── package.json ├── node_modules/ ├── .yarnrc.yml ├── package.json ├── README.md ├── turbo.json └── yarn.lock

As one can see, the root of the monorepo has node-modules and package.json, as well as every app and shared package. The problem is, Yarn’s defaults don’t reflect this structure and are incompatible with React Native and Next JS.

  • Yarn 3, uses the pnp node linker by default, which structures the node-modules folder differently. In addition, yarn will hoist shared node modules between apps and packages to the root node_modules directory

    • Issue: React Native’s Pod files (iOS) and Gradle files (android) import native modules using relative paths to the app directory and are incompatible with these two features. If we don’t disable hoisting and pnp, the relative import paths will be referencing non-existing modules because they have been hoisted outside of the app directories.

    • Solution: Each app contains its own complete node_modules folder, with no hoisting. Just like a regular repo.

  • Both Next JS and React Native, will not look for node modules and transpile source files outside of their respective app directories by default.

    • Issue: We must update next.config.js and metro.config.js to tell it where to find node modules to use. We’ll override these defaults to allow apps to follow symlinks and transpile shared modules outside of the application folder.

    • Solution: next.config.js and metro.config.js to be able to import modules from the packages directory

3. Create and Configure Yarn 3 monorepo

With these limitations in mind, we can use Yarn workspaces to create our basic monorepo structure.

  • In a fresh repo, run yarn init -2 to install Yarn 3 into our repo.

  • If exists, remove the line for !.yarn/caches in the .gitignore file, as we don’t want to track this in git.

  • Add lines to .yarnrc.yml to disable hoisting and use traditional node-modules linker instead of pnp

    { "name": "@ex-yarn-turborepo-demo/root", "version": "0.0.0", "private": true, "packageManager": "[email protected]", "workspaces": ["apps/", "configs/", "packages/*"], ... }

  • Configure package.json to add a namespaced name, enable workspaces and other misc flags.

    { "name": "@ex-yarn-turborepo-demo/root", "version": "0.0.0", "private": true, "packageManager": "[email protected]", "workspaces": ["apps/", "configs/", "packages/*"], ... }

  • Going forward, each app and package added needs a namespaced name, version, and private: true (this avoids accidental publishing to the public npm registry)

Congrats! We now have a monorepo, ready for new apps and packages!

4. Add and Configure React Native + Web apps

Now that we have a monorepo structure, we can begin to add applications

  • Application directories can be copied into the ./apps folder.

  • In this case, we will use React Native and Next JS’s boilerplate generator commands

    • Add a Typescript Next JS app (cd apps && npx create-next-app@latest --ts web-next)

    • Add a Typescript React Native app (cd apps && npx react-native init mobile-rn --template react-native-template-typescript)

  • Just like with the monorepo root, we want to add a namespaced name (such as "name": "@ex-yarn-turborepo-demo/react-native"), version, and private: true to each app’s package.json files

  • Next, we’ll configure React Native’s metro.config.js to address the limitations mentioned in the above section. To do this, we’ll ensure the monorepo root and React Native’s node_modules paths are watched

    const { resolve } = require('path');

    const rootPaths = [resolve(__dirname), resolve(__dirname, '../..')]; const nodeModulesPaths = rootPaths.map(rootPath => resolve(rootPath, 'node_modules'), );

    module.exports = { ... watchFolders: rootPaths, resolver: { nodeModulesPaths: nodeModulesPaths, }, }

  • Likewise, we’ll configure Next JS 13’s next.config.js to transpile all @ex-yarn-turborepo-demo namespaced packages found in its package.json dependencies field.

    • cd apps/next

      /** @type {import('next').NextConfig} */

      const { dependencies } = require('./package.json')

      // init const transpilePackages = Object .keys(dependencies) .filter((dependency) => dependency.includes('@ex-yarn-turborepo-demo/'))

      // main const nextConfig = { reactStrictMode: true, swcMinify: true, transpilePackages }

      // export module.exports = nextConfig

  • Lastly, we’ll want to remove any ./apps/**/.git folders and ./apps/**/package-lock.json files created by the boilerplate commands by running rm -rf ./apps/*/.git && rm -rf ./apps/*/package-lock.json

    • We remove .git folders to avoid having nested git metadata, this is a single repo with single git history after all.

    • We remove package-lock.json because yarn already has a lock file in the root of our monorepo

We should now have two applications hosted in our monorepo!

5. Add Shared Packages

With applications added to our repo, let’s utilize the power of Yarn Workspaces by adding a simple typescript package and importing it into both apps. See git repo's utils for an example.

typescript package
  • Install this package in our app

    (cd apps/react-native && yarn add @ex-yarn-turborepo-demo/utils@workspace:^) (cd apps/next && yarn add @ex-yarn-turborepo-demo/utils@workspace:^)

  • Import this package into our apps

    import { exampleExport } from '@ex-turborepo-demo'

  • That wasn’t so hard, importing shared packages is easy with Yarn 🙂

6. Set Up Turborepo and Build Scripts

At this point, we have a solid foundation to work off of: A monorepo with two apps and a shared package, let’s configure the scripts required for dev environments, building and starting the app.

  • This is where Turborepo shines.

    It can detect our dependency tree based on our imports and build/run scripts accordingly.

  • Add turbo to monorepo root using yarn add -D turbo

  • Add a turbo.json file at the base of your new repository:

    • Ensure turbo.json has dev, build and start pipelines. Pipelines tell Turborepo what yarn scripts are available to run in each app and package.

      { "$schema": "https://turborepo.org/schema.json", "pipeline": { "dev": { "cache": false }, "build": { "dependsOn": ["^build"], "outputs": ["dist/", ".next/"] }, "start": { "dependsOn": ["build"] }, "test": { "cache": false }, "lint": { "cache": false } } }

    • Notice that build and start have a dependsOn property

    • This is key:

      • The value ["^build"] with the ^ character, means to run all existing build scripts in the dependency tree. It will traverse up the dependency tree, building from the extremities, and finally the current app’s build script.

      • The value ["build"] without the ^ character, means to make sure only the build script of the current application has run before starting this pipeline.

    • Tip: we add this ^ character to enable this traversing behaviour in any script, such as ^dev

  • We’ll have to ensure each app in the ./apps folder has a yarn script with dev, build, and start. Please see git repo’s ./apps for an example. In essence, these represent the dev environment, build task and start the task of each of our apps. Scripts in our package.json for React Native could look like this:

    { ... "scripts": { "postinstall": "(cd ios && pod repo update && pod install)", "dev": "react-native run-ios", // run-android if on wi "build": "echo 'no build yet'", "start": "react-native start --reset-cache", ... }, ... }

  • We’ve added one more script for postinstall that ensures the React Native’s cocoa pods (iOS native modules) are installed whenever we run yarn install

7. Run Dev Environments and Build for Production

Finally, we’re at the stage where we can preview our work. Pat ourselves on the back for following all these steps! 🙌

  • Install all app & package dependencies and native modules using yarn install

    • Our browser and iOS emulator should pop up after some time with our apps loaded.

      • If Xcode errors occur in the console, make sure our Xcode project and emulator are set up

      • If the browser doesn’t open, open [localhost:3000 () manually in our browser

  • Run all app’s dev environments using yarn dev

  • Update and save code in our shared module, changes will propagate and build automatically.

  • Run yarn build to build bundles for production.

That’s it! With this setup, we can quickly write code in one repo that is shared between multiple apps. We’re set to explore extended features of React Native, Next, the Vercel Turborepo stack such as remote caching, pruning, parallel builds, and so on.

Conclusion

To recap, I covered how to create a monorepo with Turborepo for React Native and Next JS, went over limitations to watch out for with these tech stacks, demonstrated how to run a dev environment, and shared packages such as utilities, types, and more between the applications. Ideally, with this documentation of edge cases and core concepts, we’re able to easily start a project with a solid boilerplate for monorepos with Turborepo, without falling into common pitfalls.

Resources

written by
Logo of QuantumMob

We are a Toronto-based end-to-end digital innovation firm with a passion for building beautiful & functional products that deliver results.

The image of hire us
You might also like