« Read other posts

React Native with Typescript + Web (Vite.js) + Storybook

9 min read ·

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.

Contents

Configure React Native

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

Configure Typescript

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",

Setup Reasonable Structure

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';

Configure React Native Web with Vite

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@17

Check if the issue has been resolved first and only do this if you face issues!

Environment Variables

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)

Web

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.

Native

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.

Storybook

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.

Native

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);

Running Storybook

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

Common Component Location

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.

Web

THIS SECTION IS STILL A WORK IN PROGRESS AND DOES NOT FULLY WORK YET!

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.

Post Setup Guide

Below are a few topics which may be useful after setting up your project.

Recurring Setup

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

Sample README

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).