Shared component library for ReactJS and React Native with Storybook
A few years ago the idea of writing reusable code for different platforms was a “little” wild, right? But luckily now, in 2022, we have a lot of amazing tools that let us do this in quite a simple way. And one of the best use cases for writing reusable code is building a component UI library that is a great way to enforce design standards across an organisation or codebase. In this article, we’ll be taking a look at React Native, React JS and Storybook to try to create a reusable library for WEB and Mobile platforms.
What is React Native?
Let’s start by determining what React Native is. It’s a JavaScript framework that lets you create truly native apps for iOS and Android. This technology, similar to React for the Web, is written using a combination of JavaScript and XML markup, known as JSX. In other words, Web Developers, can now write mobile applications that look and feel truly “native,” all from the comfort of a JavaScript library that they already know and love. That’s the key thing that will help us in building the reusable library boilerplate.
What is a Storybook?
The next key piece of our application is Storybook — a JavaScript tool that allows us to create and organise UI systems making both the building process and documentation more efficient and easier to use.
For our application, we will use a Storybook for presenting the UI of our components and how they work.
The concept
Great, since we are all clear on the techstack that we are going to use, let’s dive more into the concept of our future application. The first idea that comes to mind when you are thinking about the “write once use everywhere” concept is react-native-web. This is an amazing library that lets you run React Native components and APIs on the web using React DOM (in case you created your React Native app using Expo, then this library will be included by default). A problem that could surface is a potential bottleneck in the development process, where all will be dependent on the native Engineers only, and the inability to impact or improve the web-version of components separately.
In order to overcome these limitations and add a bit of flexibility to the process, we will create a monorepo that will support ReactJS and React Native at the same time. Where the React Native will be handling the load and build of the mobile platform application using .android
, .ios
, or .native
files. And React JS — .web
files for the WEB application.
We will separate the presentation layer of the components to component.web.tsx
, component.native.tsx
and at the same time will try to reuse the logical layer using container components — component.container.tsx
that we are going to implement via the “render props” technique. And finally, we will connect the storybook to our applications to let us build and document UI components in isolation for WEB and native platforms.
As part of this tutorial, we will be following the folder structure that is being generated by the Expo, but in your project, you can use any structure you want.
The final version of this POC you can find here.
Setting up the project
Let’s dive into the actual implementation of the shared component library. We will be using the Expo for setting up the initial React Native application with Typescript.
First, we need to add Expo CLI globally by running:
yarn global add expo-cli
Now we can create our empty Expo project:
expo init ui-library
And select blank (TypeScript)
or blank
option if you want application just with JS.
Next, navigate to the project folder:
cd ui-library
And initiate the installation of react Storybook:
npx -p @storybook/cli sb init --type react
Install the Storybook React Native package:
yarn add -D @storybook/react-native
While using the storybook for React Native, I found myself repeatedly manually creating a file with imports for all of my stories. To solve this we will be using react-native-storybook-loader:
yarn add react-native-storybook-loader -D
Note: If you prefer to set things up yourself, you can follow the manual setup and skip to
Creating component
step.
Update the script section in your package.json
in order to run pre-storybook script before starting React native application:
{... "scripts": { ... "start-native": "yarn prestorybook && expo start", "prestorybook": "rnstl", ... }}
The next step in setting up a react-native-storybook-loader is the configuration in which directory to look for native stories. In the package.json
file add the following section:
{...
"config": { "react-native-storybook-loader": { "searchDir": ["./stories"], "pattern": "**/*.stories.native.tsx", "outputFile": "./storybook/storyLoader.js" } },
}
yarn prestorybook
will search in the application for the stories files following the pattern that you specified in thepackage.json
and automatically generate the list of stories files in the storyLoader.js
file.
Finally, modify your App.tsx
or App.js
file to the following:
Note: After the first run of the application the
storyLoader.js
file will be generated.
Creating component
First, let’s create a components/button
folder and add a props interface file
Then we can add our native component button.native.tsx
Making stories
Now that we have a component in our application we can create a story for it. Clean everything from the stories folder and add the simplest type of story for the button component button.stories.native.tsx
that looks like this:
Now, you can start the application by running:
yarn start-native
Here is the result that you can see:
Adding WEB support
Install the expo webpack config for the web version of the library, so we can add unimodules support and serve the ReactJS application separately from the React Native.
yarn add -D @expo/webpack-config
Create a custom webpack config:
mkdir .storybook
and
touch .storybook/webpack.config.js
And add following content:
Then modify the stories section in .storybook/main.js
file to use only web specific files for the browser version of the library:
{...
"stories": [ "../stories/**/*.stories.web.@(js|jsx|ts|tsx|mdx)" ],
}
Now let’s create ReactJS version of our component button.web.tsx
in the /components.button
folder:
And a story file for it button.stories.web.tsx
in the /stories
folder:
Now you can run:
yarn start-storybook -p 6006
And see the result:
Adding reusable components
Until this point, there wasn’t much reusable stuff between applications, but now let’s create the container component that will hold shared logical parts for both our.native
and .web
components.
Create file button.container.tsx
with the following content:
This is a point of our application where we should connect our state management system, data fetching, or any other logical part of the application. That is commonly supported in ReactJS and React Native applications.
So now we can update our stories files to see the components with reusable logic:
And
Sharing styles
In order to share styles between the platforms you can simply create style.ts
file:
export const Style = { "fontSize": 15, "paddingBottom": 20, "paddingTop": 50, "color": "#01488b", "textAlign": "center"}
And add these lines of the code to the .native
components to use a style:
const styles = StyleSheet.create({ title: Style})
And for the web version pass it directly to the style property:
<button style={Style}></button>
It would even be possible to extend the object to achieve two separate styles based on a common shared one. However, sharing styles is something that might be too hard to maintain, perhaps requiring even more time than handling two completely separate style definitions. Therefore, if you decide to venture into this quest for unification, be aware that it might not be that rewarding.
To Conclude
We’ve set-up an application that lets you create native and web components with the ability to share logical parts of it. At Property Finder, we have already benefited from this technique and we are convinced that it is a simple and very effective way of creating the cross-platform features that helps us to save time and money. We hope that you find this useful for your application as well.
Happy coding!