Sharing Platform Specific Code Between React and React Native Projects in Monorepos
15 October 2022 / 4 min read
React and React Native projects tend to have sharable code between two platforms which developers would like to make compatible for both platforms in a single import declaration. While the article focuses on NX monorepos, the same approach can be used for Turborepo/Lerna/Rush and other monorepos as well.
The Problem
Let’s imagine we have a Next.js project and a React Native project in a monorepo, and we want to share a typical wrapper library for REST requests based on fetch
API. On the one hand, the web project uses Cookies for authentication, so fetch
needs a correct header to set cookies automatically. On the other hand, the React Native project uses some token authentication such JWT, so we have to store a token in a secured storage like Keychain or encrypted react-native-mmkv
.
The problem is that we need to have two different implementations of the same library for two different platforms to properly handle authentication, so we would have two imports of almost same libraries.
React Native Library
// libs/mobile/request/src/index.ts
import MMKV from 'react-native-mmkv';
const storage = new MMKV();
export function request(url: string, options: RequestInit) {
const token = storage.getString('authToken');
return fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${token}`,
},
});
}
React Library
// libs/web/request/src/index.ts
export function request(url: string, options: RequestInit) {
return fetch(url, {
...options,
useCredentials: 'include',
});
}
The Solution
React Native supports Platform Specific Extensions which allows us to have different implementations of the same library, so Metro bundler will resolve a correct module for React Native while the Webpack (or other bundler) will resolve a correct module for the web:
library.ios.ts
- iOS specific implementationlibrary.android.ts
- Android specific implementationlibrary.native.ts
- React Native specific implementation
Library Structure
We should refactor a little bit library to expose a single module. Consider following structure of the libs/shared/request
library:
|-src
| |-lib
| | |-platform-specific.ts
| | |-platform-specific.native.ts
| | |-request.ts
| | |-types.ts
| |-index.ts
request.ts
is a common module which exports the request
function. platform-specific.ts
and platform-specific.native.ts
are implementations for each platform that we use, so we can import them in request.ts
and use in the request
function. We specifically wouldn’t like to implement two platform-specific request
function because it leads to a code duplication. Moreover, TypeScript doesn’t support custom extensions, so it’s not able to resolve our .native.ts
extension without a direct import or a project’s configuration (in case of RN it’s implemented on the Metro’s bundler level). To address this issue, we’re going to force ourselves to use the same type for exported platform-specific functions in order to reduce a risk of potential mistakes.
lib/types.ts
Contains the type for platform-specific function.
export type ExtendRequest = (options: RequestInit) => RequestInit;
lib/platform-specific.ts
import { ExtendRequest } from './types';
export const extendRequestOptions: ExtendRequest = (options) => {
return {
...options,
useCredentials: true,
};
}
lib/platform-specific.native.ts
import MMKV from 'react-native-mmkv';
import { ExtendRequest } from './types';
const storage = new MMKV();
export const extendRequestOptions: ExtendRequest = (options) => {
return {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${token}`,
},
};
}
lib/request.ts
import { extendRequestOptions } from './platform-specific';
export function request(url: string, options: RequestInit) {
const requestOptions = extendRequestOptions(options);
return fetch(url, requestOptions);
}
index.ts
export { request } from './lib/request';
Usage
Afterwards, we could import the library using a regular import:
import { request } from '@example-org/shared/request';
Conclusion
The approach described in the article is pretty straightforward, so it’s easy to use in smaller monorepos. However, you could still encounter issues if you have more than a few projects which have to reference a single shared library. In this case, I recommend designing a proper architecture instead hacking around the module resolution mechanism which is obviously not a perfect solution🙂