This is a guide to configure React Native to be built for native and web platforms with Typescript, Vite.js and Storybook support for modelling components.
Read on Website | Read on GitHub
- Configure React Native
- Configure Typescript
- Setup Reasonable Structure
- Configure React Native Web with Vite
- Environment Variables
- Storybook (Web is WIP!)
- Post Setup Guide
Setup a new React Native project:
npx react-native init MyApp
Configure your environment as according to React Native's developer guide.
Switch to Yarn Berry (if using Yarn), by following this guide.
Add .gitignore
:
# OSX
.DS_Store
# Xcode
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
project.xcworkspace
# Android/IntelliJ
build/
.idea
.gradle
local.properties
*.iml
# node.js
cache
node_modules/
package-lock.json
npm-debug.log
yarn-error.log
# BUCK
buck-out/
\.buckd/
*.keystore
# Fastlane
*/fastlane/report.xml
*/fastlane/Preview.html
*/fastlane/screenshots
# Bundle artifact
*.jsbundle
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Environment
.env
Add Typescript to your dependencies:
yarn add typescript
yarn exec tsc --init
yarn add --dev @types/react @types/react-native
Update tsconfig.json
:
- "target": "es2016",
+ "target": "esnext",
- // "lib": [],
+ "lib": [
+ "es2017",
+ "ES6"
+ ]
- // "jsx": "preserve",
+ "jsx": "react-native",
- // "allowJs": true,
+ "allowJs": true,
- // "noEmit": true,
+ "noEmit": true,
- // "isolatedModules": true,
+ "isolatedModules": true,
- // "moduleResolution": "node",
+ "moduleResolution": "node",
Delete App.js
.
Create a folder src
with a new file src/App.tsx
:
import React from "react";
import { Platform, Text } from "react-native";
export default function App() {
return (
<Text>Hello World! Your platform is { Platform.OS }</Text>
)
}
Update index.js
:
- import App from './App';
+ import App from './src/App';
Install Vite.js dependencies in root:
yarn add react-native-web react-dom
yarn add --dev vite @vitejs/plugin-react @types/react-dom
Add scripts to package.json
for starting with Vite:
{
"scripts": {
[...],
"web": "vite -c web/vite.config.ts",
"build:web": "tsc && vite build -c web/vite.config.ts",
"preview:web": "vite preview -c web/vite.config.ts"
}
}
Initialise the Vite.js project:
yarn create vite web --template react-ts
Delete web/src
folder and delete web/package.json
.
Create a new web/main.jsx
file:
import React from "react";
import ReactDOM from "react-dom";
import App from "../src/App";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root"),
);
Update web/index.html
:
- <script type="module" src="/src/main.tsx"></script>
+ <script type="module" src="./web/main.tsx"></script>
Add recommended styling to web/index.html
(as-per RN Web documentation):
<style>
/* These styles make the body full-height */
html,
body {
height: 100%;
}
/* These styles disable body scrolling if you are using <ScrollView> */
body {
overflow: hidden;
}
/* These styles make the root element full-height */
#root {
height: 100%;
}
</style>
Then move web/index.html
to index.html
.
Configure RN Web in web/vite.config.ts
:
export default defineConfig({
[...],
resolve: {
alias: {
"react-native": "react-native-web",
},
},
})
Update web/tsconfig.json
to include the main entrypoint and allow ES inter-op:
- "include": ["src"],
+ "include": ["."],
- "esModuleInterop": false,
+ "esModuleInterop": true,
Also append the following:
{
"compilerOptions": {
[...],
"types": ["vite/client"]
}
}
You may need to lock React to version 17 as of writing (5th April 2022), React Native Web does not fully support React 18 yet. See issue here.
yarn add react@17 react-dom@17Check if the issue has been resolved first and only do this if you face issues!
Loading environment variables varies between platform, on the web you'll be accessing import.meta
but on native devices, you'll need to use react-native-dotenv
.
This guide will show you how to load environment variables in both situations, but it is up to you to figure out how to create a unified API for accessing them isomorphically. Two ways you could go about this are:
- Pass environment variables when constructing
<App />
(easy) - Create a new module which manages your environment variables that you can inject into (global access)
To use environment variables in Vite, you can use import.meta
whenever you need access, for example:
// It is important to note that environment variables
// that are pushed into Vite, MUST be prefixed with VITE_.
let v = import.meta.env.VITE_SOME_VARIABLE;
Environment variables can be provided through .env
, .env.xxxx
and the actual environment. See more information here.
To access environment variables on native devices, we can configure react-native-dotenv.
Install the package:
yarn add react-native-dotenv
Create .babelrc
and populate with:
{
"plugins": [
["module:react-native-dotenv"]
]
}
Create a new file types/env.d.ts
:
declare module '@env' {
// You can define your variables here:
export const STORYBOOK: string;
export const ONLY_AVAILABLE_ON_NATIVE: string;
export const VITE_VARIABLE_FOR_BOTH_PLATFORMS: string;
}
You can now use variables by importing from @env
:
import { STORYBOOK } from '@env';
Environment variables can only be provided through the dotenv file since these are injected into the binary, read more here about the configuration.
Ideally, you want to configure Storybook so that you can use it on both the web and your native devices, we start off by configuring it natively.
First, we follow the official guide for Storybook:
npx -p @storybook/cli sb init --type react_native
Update and append new scripts for starting the React Native bundler in package.json
:
{
"scripts": {
"start": "APP_ENV=development react-native start --reset-cache",
"start:storybook": "APP_ENV=storybook react-native start --reset-cache",
"start:previous": "react-native start",
}
}
Due to the way react-native-dotenv
works, we need to clear cache whenever changing the environment.
Explanation of the new scripts:
start
: Start Metro bundler, read from.env.development
and reset the cache.start:storybook
: Start Metro bundler, read from.env.storybook
and reset the cache.start:previous
: Start Metro bundler and re-use previously cached files, but any changes may reset the loaded environment variables. (this method will start the fastest if you need restart Metro for any reason)
Now create .env.development
:
STORYBOOK=0
Now create .env.storybook
:
STORYBOOK=1
Finally, we need to update index.js
to support conditionally loading Storybook:
import { AppRegistry } from "react-native";
import App from "./src/App";
import { name as appName } from "./app.json";
import { STORYBOOK } from '@env';
import StorybookUI from './storybook';
AppRegistry.registerComponent(appName, () => STORYBOOK === '1' ? StorybookUI : App);
You can now drop into Storybook by running yarn storybook
and yarn start:storybook
(along with yarn android
or yarn ios
).
If it appears to not be connecting, you may need to run:
adb reverse tcp:7007 tcp:7007
Before we can setup Storybook for Web, we must move the stories to a common location that can be accessed by both and make sure the files are correctly named so that Vite can compile them.
To begin, move storybook/stories
to stories
or a folder of your choosing (components
is also a good name). Your IDE should automatically update imports, but if it doesn't, go into storybook/index.js
and update:
- require('./stories');
+ require('../stories');
Now we must update all of the file extensions for the actual components and stories from .js
to .tsx
, but do not change the extension of index.js
!
Fixing type errors is left up to an exercise to the reader, realistically though you're probably replacing these files anyways.
To setup Storybook, we're going to make a sub-project with a fresh React project.
Because the Create React App team did not account for edge cases where you need to create an independent project, you will need to find a new directory outside of the project root from which we will create this then later copy it in.
yarn create react-app storybook-web
cd storybook-web
npx sb init --type react
# if prompted, say yes to fixing the project
yarn
Now copy storybook-web
back into your project at the root.
Edit storybook-web/.storybook/main.js
:
- "../stories/**/*.stories.mdx",
- "../stories/**/*.stories.@(js|jsx|ts|tsx)"
+ "../../stories/**/*.stories.mdx",
+ "../../stories/**/*.stories.@(js|jsx|ts|tsx)"
In that same file, add this at the bottom of exports:
module.exports = {
...,
async viteFinal(config, { configType }) {
config.module.rules.push({
test: /\.tsx?$/,
exclude: /node_modules/,
use: [
{
loader: require.resolve('babel-loader'),
options: {
presets: [
require('@babel/preset-typescript').default,
[require('@babel/preset-react').default, { runtime: 'automatic' }],
require('@babel/preset-env').default,
],
},
},
],
})
config.resolve.extensions.push('.ts', '.tsx')
config.resolve.alias = {
'react-native': 'react-native-web',
'@storybook/react-native': '@storybook/react'
}
return config;
},
};
This is as far as I could get, Storybook would fail to load components with a cryptic error message.
Below are a few topics which may be useful after setting up your project.
Whenever you pull the project to a new device and intend to build for Android, you must regenerate the debug keystore:
keytool -genkey -v -keystore android/app/debug.keystore -storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000
Below is a sample README you can copy into your project:
# Your App
The app is powered by React Native and builds for both mobile and the web, this repository is setup using the [guide available here](https://gist.github.com/insertish/9cca9b6aa75a7cf34d050368d067ecf5).
## Get Started
First, [configure your environment](https://reactnative.dev/docs/environment-setup#development-os).
Clone and run locally:
\```sh
git clone https://github.com/your-org/repo App
cd App
yarn
yarn start
\```
> When starting the bundler in the future, use `yarn previous`, unless if you are also using the Storybook, in which case you need to run `yarn start` at least once when switching back.
If building for an Android device, [reconfigure your keystore](https://gist.github.com/insertish/9cca9b6aa75a7cf34d050368d067ecf5#recurring-setup).
Now, launch the app on your phone:
\```sh
# Launch Android app
yarn android
# Launch iOS app (requires Mac)
yarn ios
\```
### Launching Web app
To launch the web app, we can skip using the Metro bundler:
\```sh
# Launch Vite.js bundler
yarn web
\```
### Storybook
This repository uses Storybook for previewing components.
Instructions on running [can be found here](https://gist.github.com/insertish/9cca9b6aa75a7cf34d050368d067ecf5#running-storybook).
Thanks so much for this, would you happen to know how I could transpile other react native modules for web using this method?