THE FINNTERNET

Next.js in React-Native Monorepo

9/30/2021

TL;DR

After adding TV support to Matteo Mazzarolo's monorepo, the only platform missing (that I care about) is Next.js.

At anytime, if you get the lstat error when trying to install something, just rerun the command. It seems like a common issue that pops up in yarn 1 workspaces sometimes.

Initialize Next App

% cd packages
% yarn create next-app --typescript
✔ What is your project named? … next
% cd next
% yarn dev
% yarn add react-native-web @react-native-async-storage/async-storage
% yarn add -D babel-plugin-react-native-web babel-plugin-transform-define @types/react-native

Update package.json

Let's add a package name, version, and our app dependency.

{
	"name": "@my-app/next",
	"version": "0.0.1",
	...
	"dependencies": {
		"@my-app/app": "*",
		...	
	},
	...
}
% yarn

Replace Next App with the Monorepo App

Let's replace the current Next app with our monorepo app and also polyfill setImmediate.

% yarn add setimmediate
// packages/next/pages/index.tsx
import 'setimmediate';
import { App } from '@my-app/app';

export default App;

Configure Next Document

We need to update _document.tsx to register our app and handle the root styling (e.g. expanding the root div to 100% height).

// packages/next/pages/_document.tsx
import * as React from 'react'
import { Children } from 'react'
import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'
import { AppRegistry } from 'react-native'

import { name as appName } from '../package.json'

// Force Next-generated DOM elements to fill their parent's height
const normalizeNextElements = `
  #__next {
    display: flex;
    flex-direction: column;
    height: 100%;
  }
  html {
    height: 100%;
  }
  body {
    height: 100%;
    overflow: hidden;
  }
`

export default class MyDocument extends Document {
  static async getInitialProps({ renderPage }: DocumentContext) {
    AppRegistry.registerComponent(appName, () => Main)
    const { getStyleElement } = AppRegistry.getApplication(appName)
    const page = await renderPage()
    const styles = [
      <style key='normalizeNextElements' dangerouslySetInnerHTML={{ __html: normalizeNextElements }} />,
      getStyleElement(),
    ]
    return { ...page, styles: Children.toArray(styles) }
  }

  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

Update next.config.js

In order to work with the monorepo and react-native-web, we need to do a few things,

  • Enable the experimental externalDir feature (or we can use next-transpile-modules).
  • Alias react and react-native-web so that Next can resolve them.
  • Add plugins to handle static asset imports.
% yarn add next-images next-fonts
// packages/next/next.config.js
/** @type {import('next').NextConfig} */
const path = require("path");

// Necessary to handle statically imported images
const withImages = require('next-images');
// Necessary to handle statically imported fonts
const withFonts = require('next-fonts');

module.exports = withImages(withFonts({
  // Allows us to access other directories in the monorepo
  experimental: {
    externalDir: true
  },
  // This feature conflicts with next-images
  images: {
    disableStaticImages: true,
  },
  webpack: (config, options) => {
    if (options.isServer) {
      config.externals = ['react', 'react-native-web', ...config.externals];
    }
    config.resolve.alias['react'] = path.resolve(__dirname, '.', 'node_modules', 'react');
    config.resolve.alias['react-native-web'] = path.resolve(__dirname, '.', 'node_modules', 'react-native-web');

    return config;
  }
}));

Configure Babel

We need to create a custom babel.config.js in order to handle react-native-web as well as to define __DEV__ and __SUBPLATFORM__.

// packages/next/babel.config.js
module.exports = {
  presets: ['next/babel'],
  plugins: [
    ['react-native-web', { commonjs: true }],
    ['transform-define', {
      '__DEV__': process.env.NODE_ENV,
      '__SUBPLATFORM__': 'next',
    }]
  ],
}

Configure tsconfig.json

We need to extend from the root tsconfig.json and keep the necessary settings for Next.

// packages/next/tsconfig.json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "jsx": "preserve",
    "module": "esnext"
  },
  "include": [
    "next-env.d.ts",
    "public",
    "pages"
  ],
  "exclude": [
    "node_modules"
  ]
}

Add Root Scripts

Now, we can add some scripts to our root package.json so we can use the Next app from the monorepo root.

// package.json
{
	"scripts": {
		...
			"next:start": "yarn workspace @my-app/next dev",
	    "next:build": "yarn workspace @my-app/next build",
	    "next:serve": "yarn workspace @my-app/next start",
		...	
	}
}

Closing Thoughts

Next has always been my goto for web projects so I'm excited to see how the DX is from react-native. The navigation in Next differs from the normal react-navigation flow that fits well with most of the other platforms, but Fernando Rojo's expo-next-react-navigation aims to bridge this gap.

Chris' bitmoji smiling

Hi, how's it going? I’m Chris Finn, a full-stack developer from Boston, MA. I specialize in UI and UX design, cross-platform app development, and eating calzone. I am also a sucker for automation and security (in a past life I was a cybersecurity researcher).