About: software engineer. writes at blog.adebayo.dev
Joined:
Feb 12, 2020
Building a universal React app with Expo, Next.js & Nativewind
Publish Date: Jul 2 '24
57 5
Introduction
Building universal React applications has never been easier or more efficient, thanks to Expo. Expo is a powerful toolchain that simplifies the development process, allowing developers to create high-quality, performant apps for iOS, Android, and the web with a single codebase.
With this guide, we will set up a monorepo from scratch to build a Universal React app using Expo and Next.js using tools like NativeWind/Tailwind, Turborepo for building apps across both mobile and web platforms.
Problem
At my job, I was assigned the task of building a design system for both our mobile and web products. Given my background as a React developer, React Native was the natural choice for mobile development.
The challenge was to create a shared component library with consistent styling that works seamlessly across both mobile and web applications using React and React Native.
The goal was to develop a solution that supports the development of both mobile and web applications without duplicating components, rewriting business logic, or maintaining separate codebases.
Before we get started, let's familiarise ourselves with some key terminologies.
Universal in this case means it works on all platforms i.e Andriod, IOS, Web and others.
We currently have first-class support for Yarn 1 (Classic) workspaces. If you want to use another tool, make sure you know how to configure it.
yarn set version 1.22.19
Set private flag as true
+ "private": true
Note that the private: true is required, Workspaces are not meant to be published.
Create sub folders apps and packages
"workspaces":["apps/*","packages/*"],
packages/* simply means we'll reference all packages from a single directive
apps contains
web
native
packages contains
ui
utils e.t.c
Install Turborepo
turbo is built on top of Workspaces, a feature of package managers in the JavaScript ecosystem that allows you to group multiple packages in one repository.
/** @type {import('next').NextConfig} */const{withExpo}=require("@expo/next-adapter");module.exports=withExpo({reactStrictMode:true,transpilePackages:[// NOTE: you need to list `react-native` because `react-native-web` is aliased to `react-native`."react-native","react-native-web","ui"// Add other packages that need transpiling],webpack:(config)=>{config.resolve.alias={...(config.resolve.alias||{}),// Transform all direct `react-native` imports to `react-native-web`"react-native$":"react-native-web","react-native/Libraries/Image/AssetRegistry":"react-native-web/dist/cjs/modules/AssetRegistry"// Fix for loading images in web builds with Expo-Image};config.resolve.extensions=[".web.js",".web.jsx",".web.ts",".web.tsx",...config.resolve.extensions];returnconfig;}});
Resetting React Native Web styles
The package react-native-web builds on the assumption of reset CSS styles, here's how you reset styles in Next.js
Add ui package in both native and web dependencies in package.json file
...."ui":"*",....
Replace apps/native/index.tsx in Expo app
import{Text}from"react-native";import{View}from"ui/view";exportdefaultfunctionIndex(){return (<Viewstyle={{flex:1,justifyContent:"center",alignItems:"center",}}><Text>Edit app/index.tsx to edit this screen.</Text></View>);}
Replace apps/web/index.tsx in Next.js app
"use client";import{View}from"ui/view";exportdefaultfunctionHome(){return (<Viewstyle={{flex:1,justifyContent:"center",alignItems:"center",}}><p>
Get started by editing <codeclassName="font-mono font-bold">app/page.tsx</code></p></View>);}
Configuring Metro bundler
To configure a monorepo with Metro manually, there are two main changes:
Wee need to make sure Metro is watching all relevant code within the monorepo, not just apps/native.
cd apps/native
npx expo customize metro.config.js
Update metro.config.js
// Learn more https://docs.expo.io/guides/customizing-metroconst{getDefaultConfig}=require("expo/metro-config");constpath=require("path");constworkspaceRoot=path.resolve(__dirname,"../..");constprojectRoot=__dirname;constconfig=getDefaultConfig(projectRoot);config.watchFolders=[workspaceRoot];config.resolver.nodeModulesPaths=[path.resolve(projectRoot,"node_modules"),path.resolve(workspaceRoot,"node_modules")];config.resolver.disableHierarchicalLookup=true;module.exports=config;
import{registerRootComponent}from"expo";import{ExpoRoot}from"expo-router";// Must be exported or Fast Refresh won't update the contextexportfunctionApp(){constctx=require.context("./app");return<ExpoRootcontext={ctx}/>;
}registerRootComponent(App);
Now, we have a working native and web app using shared component with RNW
Results
Universal Styling with NativeWind
Next, we want to further and style both platform using Tailwind in Next.js and on mobile, NativeWind is the right tool to achieve.
NativeWind allows you to use Tailwind CSS to style your components in React Native. Styled components can be shared between all React Native platforms.
cd apps/native
npx expo install nativewind@^4.0.1 react-native-reanimated tailwindcss
Run pod-install to install Reanimated pod:
npx pod-install
Run npx tailwindcss init to create a tailwind.config.js file
npx tailwindcss init
Add the paths to all of your component files in your tailwind.config.js file.
NativeWind extends the React Native types via declaration merging. Add triple slash directive referencing the types. Add a file app-env.d.ts in apps/native root directory.
Thank you for the detailed walk through. It's nice to understand some of the reasoning behind each of the steps involved in setting this sort of repo up. Even though there are several example repo's around, they don't really tell you why things are set up the way they are so this was useful. Thanks!
thanks , the articles misses some step but the repo fixes it. this was what i needed