One codebase for React and React-native with Nx monorepos

Omal Perera
Ascentic Technology
10 min readOct 28, 2023
Image from https://www.voiceflow.com/blog/how-monorepos-can-help-improve-your-productivity

A monorepo is a single repository containing multiple distinct projects, with well-defined relationships. This shouldn’t be confused with monolithic architecture, which is a software development practice for writing self-contained applications.

Monorepo is a technical concept; Using it for your project depends on your tech stack, requirements, team arrangement, release pipeline, and many other factors. Therefore, it is always advised to analyze your system and feasibility before making a decision. This informative article, ‘What is monorepo? (and should you use it?)’, highlights some key reasons to both love and hate monorepos.

Today we will be focusing on how to maintain and share code between react and react native projects using Nx workspace.

Why monorepo?

Simple use-case

For this article, our objective is to develop a React Native mobile app and a React web app using TypeScript. Both the mobile and web projects will feature a simple counter app, and we will employ Redux for state management.

Analysing the requirement

Upon reviewing the straightforward requirement above, we can glean insights regarding code reusability. It’s evident that we cannot reuse or share the view layer components between React and React Native. However, the remainder of our TypeScript utility functions, type definitions, models, theming, service layer implementations, reducers, store, and related actions can be shared between the web and mobile applications.

In order to implement this, Nx provides robust support for easily scaffolding projects, resolving files and modules, running, building, and more.

Let’s get started with Nx

Step 1 — Setting up Nx workspace

First, you should install Nx globally by using the following command. If you encounter any errors, you can refer to the official installation instructions available here.

npm install -g nx@latest

Nx offers convenient plugins for both VSCode and JetBrains IDEs. By installing these plugins in either VSCode or JetBrains, you can streamline your workflow and reduce your reliance on CLI commands. You can find the official installation instructions here.

Install Nx Console to VSCode from the marketplace.

Step 2 — Creating a Workspace & Generating React Native & React Apps

Now, let’s proceed to create an Nx workspace, as demonstrated in the GIF below. We will create a workspace (I have named it “m-repo”) and a React Native project called “mobile.”

After following the steps above, your workspace structure should look like this, with the React Native app codebase located inside the ‘apps/mobile’ directory.

Creating the React Project in the Same Workspace

To add a React project to the Nx workspace, use the ‘generate’ command. Navigate to the Nx button in VSCode, and follow the steps shown in the GIF below. Specify the directory as apps/webApp since we’re creating our React app inside the ‘apps’ folder. Additionally, set the end-to-end test runner to ‘none’ to keep things simple, as we don't need an end-to-end test project at this time.

Generating a React project using ‘generate’ command in nx workspace

Run the React-native apps and React app

Navigate to the Nx plugin button and select the options shown below to run the specific app.

use these options to run the apps

How to remove a project from Nx workspace?

The recommended way to delete or remove a generated project in Nx workspace is through the plugin or command. Manually deleting the project folder is not a safe approach and can lead to project corruption.
You can find the specific project under the ‘Projects’ section in the Nx plugin pane and remove it by right-clicking on the project to access the deletion option.

Removing a project. (mobile-e2e project removed from Nx)

Step 3 — Creating a Shared project and use it in mobile and web projects.

Now, it’s time to create a shared project for common store since our goal is to share them between the web and mobile applications. To keep things organized, we’ll place this library inside a ‘shared’ folder in the project root, anticipating the need for more common projects as the project progresses.

For this task, you can utilize the same ‘generate’ Nx option to create a project. However, this time, it will be a library, not an application as before. Detailed steps can be found in the following GIF.

To ensure everything is working smoothly, let’s import and utilize the store library in both React Native and React. If you inspect shared/store/lib/store.ts you will find a simple function that returns the string ‘store’. We will now integrate this into our mobile and web apps.

Import and usage in mobile projects

To integrate the shared module into your mobile project, Change the content of apps/mobile/src/app/App.tsx as follows. In line number 3 we have imported our pre-created shared module. This is made possible through the configurations in tsconfig.base.json at the project root (as illustrated in the image below). Nx automatically includes the relative path of the shared project, which Metro will resolve during the build process.

//apps/mobile/src/app/App.tsx
import React from 'react';
import { SafeAreaView, StyleSheet, View, Text } from 'react-native';
import { store } from '@m-repo/store'; //This is how import works

export const App = () => {
return (
<>
<SafeAreaView />
<View>
<Text style={styles.textLg}>Welcome Mobile</Text>
<Text style={styles.textLg}>
{'From shared module -> ' + store()}
</Text>
</View>
</>
);
};
const styles = StyleSheet.create({
textLg: {
fontSize: 24,
padding: 24,
},
});

export default App;
tsconfig.base.json — Automatically added record when a library is created. @m-repo/store can be used in import statements instead of relative paths

Import and usage in Web projects

You can import the shared store string into your React project just as we did for React Native above. Replace the code in apps/webApp/src/app/app.tsx with the following code.

// apps/webApp/src/app/app.tsx
import { store } from '@m-repo/store';

export function App() {
return (
<div>
<h1>
<span>Hello there</span>
</h1>
<span>{'From shared module -> ' + store()}</span>
</div>
);
}

export default App;

Now you can run the projects and check whether all is good up to now.

running of 2 apps should show a similar result.

If you can see the string ‘store’ that we imported from the shared module, congratulations! 🎉 .
This marks the foundation for code sharing, and the next steps involve implementing the planned features.

Okay, what is next?

Comparing with the initial requirements and what we have built so far, we have accomplished the following:

  • Created an Nx workspace.
  • Developed two separate projects for React Native and React.js.
  • Established a simple shared module used in both mobile and web projects.

What remains to be done?

  • Set up and configure the Redux store and reducers.
  • Create a simple UI in both projects and connect the store.

Setting up a common store

Firstly, We need to install Redux-related dependencies in the root project.

npm i redux react-redux @reduxjs/toolkit

Create a new file named ‘counterReducer.ts’ in shared/store/src/lib/ and add the following code to create a simple store slice for storing our counter value. The updateCounteraction will be called from the component to update the value.”

// shared/store/src/lib/counterReducer.ts
import { createSlice } from '@reduxjs/toolkit';

export type TcounterReducer = {
counterValue: number;
};

const INITIAL_STATE: TcounterReducer = {
counterValue: 0,
};

const counterSlice = createSlice({
name: 'counter',
initialState: INITIAL_STATE,
reducers: {
updateCounter: (state, action) => {
state.counterValue = action.payload;
},
},
});

export const { updateCounter } = counterSlice.actions;
export const counterReducer = counterSlice.reducer;

It’s time to create the store. Open the shared/store/src/lib/store.ts file and replace its content with the following code. In this step, we configure our store.
For this demonstration, we won’t be injecting any middleware. If you plan to use different types of middleware for mobile and web, it’s advisable to configure separate stores for each project, while still sharing reducer slices.

// shared/store/src/lib/store.ts
import { combineReducers } from 'redux';
import { configureStore } from '@reduxjs/toolkit';
import { counterReducer } from './counterReducer';

const rootReducer = combineReducers({
counterReducer: counterReducer,
});

export const store = configureStore({
reducer: rootReducer,
});

export type RootState = ReturnType<typeof store.getState>;

lastly, we should expose these via the shared/store/src/index.ts so that we can import using @m-repo/store statement.

// shared/store/src/index.ts
export * from './lib/store';
export * from './lib/counterReducer';

Adding a simple UI and using the shared store

Let's do the mobile-first.

Create a new file named ‘CounterComponent.ts’ in apps/mobile/src/app/features/ and add the following code, which creates the UI for a simple counter app. This code includes a function that renders ‘+’ and ‘-’ buttons, as well as a text component to display the current counter value. when the button is pressed, an incremented or decremented value will be passed to the action we created in the “counterReducer”.

// apps/mobile/src/app/features/CounterComponent.ts
import React from 'react';
import {
SafeAreaView,
StyleSheet,
View,
Text,
TouchableOpacity,
} from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { RootState, updateCounter } from '@m-repo/store';

export const CounterComponent = () => {
const dispatch = useDispatch();
const counterValue = useSelector(
(state: RootState) => state.counterReducer.counterValue
);

const renderButton = (operation: '+' | '-') => {
return (
<TouchableOpacity
style={styles.btnBg}
onPress={() => {
dispatch(
updateCounter(
operation === '+' ? counterValue + 1 : counterValue - 1
)
);
}}
>
<Text style={styles.btnTxt}>{operation}</Text>
</TouchableOpacity>
);
};

return (
<>
<SafeAreaView />
<View style={styles.container}>
{renderButton('-')}
<Text style={styles.counterTxt}>{counterValue}</Text>
{renderButton('+')}
</View>
</>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
justifyContent: 'space-evenly',
alignItems: 'center',
marginVertical: 40,
},
btnBg: {
backgroundColor: '#FE9505',
height: 76,
width: 76,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 38,
},
btnTxt: {
fontSize: 32,
fontWeight: '600',
color: 'white',
},
counterTxt: {
fontSize: 32,
},
});

export default CounterComponent;

Next, we need to wrap our entire component tree with the ‘react-redux’ provider. To do that, open ‘App.tsx’ and replace the existing code with the following code. As you can see the ‘store’ is imported from @m-repo/store .

// apps/mobile/src/app/App.tsx
import React from 'react';
import CounterComponent from './features/CounterComponent';
import { Provider } from 'react-redux';
import { store } from '@m-repo/store';

export const App = () => {
return (
<Provider store={store}>
<CounterComponent />
</Provider>
);
};

export default App;

Adding the React js web UI and integrating the shared store

Create a new file named ‘counterComponent.tsx’ in apps/webApp/src/app/ and add the following code.

// apps/webApp/src/app/counterComponent.tsx
import { RootState, updateCounter } from '@m-repo/store';
import { useSelector, useDispatch } from 'react-redux';

const CounterComponent = () => {
const dispatch = useDispatch();
const counterValue = useSelector(
(state: RootState) => state.counterReducer.counterValue
);

const renderButton = (operation: '+' | '-') => {
return (
<button
style={styles.btnBg}
onClick={() => {
dispatch(
updateCounter(
operation === '+' ? counterValue + 1 : counterValue - 1
)
);
}}
>
<span style={styles.btnTxt}>{operation}</span>
</button>
);
};

return (
<div style={styles.container}>
{renderButton('-')}
<span style={styles.counterTxt}>{counterValue}</span>
{renderButton('+')}
</div>
);
};

export default CounterComponent;

const styles = {
btnBg: {
backgroundColor: '#FE9505',
width: 76,
height: 76,
borderRadius: 38,
borderWidth: 0,
},
counterTxt: {
fontSize: '32px',
},
btnTxt: {
fontSize: '32px',
fontWeight: 'bold',
color: 'white',
},
container: {
display: 'flex',
justifyContent: 'space-evenly',
alignItems: 'center',
},
};

Same as we did in react-native app.tsx, we need to wrap the entire component tree with a react-redux provider as below. You can replace the code in apps/webApp/src/app/app.tsx

// apps/webApp/src/app/app.tsx
import { Provider } from 'react-redux';
import CounterComponent from './counterComponent';
import { store } from '@m-repo/store';

export function App() {
return (
<Provider store={store}>
<CounterComponent />
</Provider>
);
}

export default App;

That is it! Now we can run our applications to check the behavior.

React-naive app (left side) and the React web (right side), which share the same code for ‘redux-store’ in a single repository

Things we learned today

In this article, we successfully developed both a React.js web application and a React Native app within a single repository using Nx. This structure enables us to conveniently store our common business logic in technology-independent libraries, which can be easily linked to both our React web and React Native mobile applications.

The directory and file names used in this article can be customized to fit your project’s requirements. For instance, the ‘shared’ folder can be renamed to anything that suits your needs. And also, Nx is not limited to React and React Native, but it also supports many other languages and frameworks.

Thank you for being with us throughout the article and for sharing your thoughts and experiences.

Codebase: https://github.com/OmalPerera/m-repo

For further reading

  1. Misconceptions about Monorepos: Monorepo != Monolith : https://blog.nrwl.io/misconceptions-about-monorepos-monorepo-monolith-df1250d4b03c
  2. What is monorepo? (and should you use it?) : https://semaphoreci.com/blog/what-is-monorepo
  3. Guide to Monorepos for Front-end Code: https://www.toptal.com/front-end/guide-to-monorepos
  4. Why Nx : https://nx.dev/getting-started/why-nx
  5. Share code between React Web & React Native Mobile with Nx: https://blog.nrwl.io/share-code-between-react-web-react-native-mobile-with-nx-fe5e22b5a755
  6. Step-by-Step Guide to Creating an Expo Monorepo with Nx : https://blog.nrwl.io/step-by-step-guide-to-creating-an-expo-monorepo-with-nx-30c976fdc2c1

--

--