Code Sharing with React Native & Webpack
Published: 2021-03-29 13:10:17

Time is a crucial factor in modern software development – clients need features delivered in a flash. Time – we could always do with more of it. As software engineers, though, we are often faced with such situations and have developed strategies to better use time. We work to reduce the amount we spend on unnecessary tasks. On repetitive work. On duplication of effort.

One target of such efforts is code duplication – something of particular relevance for multi-platform applications.

Code sharing is a technique that enables applications to reuse the same code across desktop, web, and mobile platforms. When done successfully, an application does not sacrifice its native look and feel on each platform. As it’s a large topic, I will limit the discussion in this post to sharing a single component across multiple platforms. Also, in reality, it’s a more likely scenario – one where we want to add a shareable component to a long-running project with an established codebase.

A conservative approach to code sharing would be to share business logic, for example, a set of JavaScript functions in a common Node module, and develop the UI of the application separately. However, this approach would not fulfil our primary objective. We would still need to implement the UI from scratch, using two different technologies, making one developer create the same feature twice, or even worse – engaging two developers for the same job.

Instead, I’ll go with a strategy based on the [**React Native**](https://reactnative.dev/) framework that has more code sharing potential. It enables developers to create mobile applications in JavaScript and deliver them as native applications with a native look and feel.

But the best feature of React Native is yet to be fully utilised in the programming world. It is the package called [**React Native Web**](https://github.com/necolas/react-native-web) that allows web developers to use React Native components and run on the web using React DOM. React Native Web is widely used in the React Native community and makes it easy to reuse components used to create mobile React Native applications in ReactJS web apps. Let’s create an example component and see what it takes to “learn once, write everywhere”.

## Project Structure

In this post we’ll only be talking about a single component. In the real world, it would likely be part of a larger component library. We’ll be using TypeScript, but there’s nothing preventing you from using JavaScript should you wish – obviously you’d need to change the configuration and webpack loaders though. Apart from including typings and creating a need for _tsconfig.json_ file, this assumption also matters when it comes to choosing appropriate webpack loaders.

Let’s create the main structure of our project:

```
-src/
-web/
-package.json
-tsconfig.json
-webpack.config.js
```

The UI and login of our shareable component will sit in the _src_ directory. We create a simple round button which will accept _onPress callback_ and _imageSource_ as properties:

_src/components/Button.tsx:_

```javascript
import React from "react";
import { TouchableOpacity, StyleSheet, ImageSourcePropType } from "react-native";

Interface ButtonProps {
  onPress: () => void
  imageSource: ImageSourcePropType
}

export default function Button({ onPress, imageSource }: ButtonProps) {
  return (
    <TouchableOpacity style={styles.button} onPress={onPress}>
      <Image
        style={styles.image}
        source={imageSource}
      />
    </TouchableOpacity>
  );
}

const styles = StyleSheet.create({
  container: {
    width: 56,
    height: 56,
    borderRadius: 28,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#fff",
  },
  image: {
    width: 28,
    height: 28,
  },
});
```

We will keep all the components in the src directory. This way, every time someone installs the package, the code will appear in _node_modules_ of the mobile React Native app. The other thing we could do would be to build it into a separate _lib_ directory, for example with the _tsc_ command, but we’re not going to do that here today.

The other important directory in the project tree is web. This is a place where we will keep the JavaScript component bundle. This is important because we can’t rely on React Native and React Native Web being installed in the React app, so in order to use our component in a web application we should have a pure React version of it prepared. How to make this bundle is discussed in the next section – for now it is important to note we will have a separate directory for the web.

We should also have a way to differentiate between the web, bundled version and React Native version of the component when importing from our library. We will do it by differentiating import paths. For web applications, we will be using _/web_ directory to import, as presented below. For the React Native we will use the root directory represented by the name of the library. We should not have one entry point for it, because that will bring a risk of importing the bundle in React Native or importing React Native version in the React app. This is why inside the web directory we will add a new _package.json_ file:

_web/package.json_:

```javascript
{
  "main": "build/bundle.web.js",
  "types": "build/src/index.d.ts"
}
```

_bundle.web.js_ file is the file with React bundle and the entry point to our components’ library from within the web application. Because we have put _package.json_ inside the web directory, if we want to use the _Button_ component in the React web application we will import it like:

```javascript
/**
* importing from build/bundle.web.js under the hood
*/

import { Button } from 'our-components-library/web'
```

While in React Native app it will simply be:

```javascript
import { Button } from 'our-components-library'
```

Except from _src_ and _web_ directories, in the project root we also defined three files:

1.  _package.json_ as the main configuration file of the whole library, containing dependencies, scripts, entry point for React Native etc.
2.  _tsconfig.json_ with definitions of typescript rules we use in the app
3.  _webpack.config.js_ with settings for bundling the code for web usage.

We will now take a closer look at the last file as bundling for the web is one of the focal points of the whole process of creating sharable components.

## Bundling React Native for web

To bundle the code I will use the most common solution by far – [**Webpack**](https://webpack.js.org/). In order to use it we first need to install a couple of modules. Depending on preference you could use Yarn or NPM, I will use Yarn in the examples. We install to dev dependencies using command:

```javascript
yarn add --dev webpack webpack-cli
```

Because we have installed the _webpack-cli_ package, we can now use the _webpack_ command to bundle our code. Let’s do it and also let’s add a script to _package.json_ and name it accordingly so other developers can easily identify the purpose of it.

_package.json:_

```javascript
"scripts": {
   "build-react": "webpack"
 }
```

Now, the most important part of the bundling process – we need to define configuration file, so the Webpack knows how to translate our React Native code into a React bundle. Let’s first take a look at the whole config and go through it section-by-section later.

_webpack.config.js:_


```javascript
const path = require("path");
 
module.exports = {
 entry: path.join(__dirname, "src/index.ts"),
 output: {
   filename: "bundle.web.js",
   path: path.join(__dirname, "/web/build"),
   library: "our-components-library",
   libraryTarget: "umd",
   umdNamedDefine: true,
 },
 module: {
   rules: [
     {
       test: /\.(ts|tsx)?$/,
       include: path.resolve(__dirname, "src"),
       exclude: /node_modules/,
       use: [
         {
           loader: "babel-loader",
           options: {
             cacheDirectory: true,
             presets: ["module:metro-react-native-babel-preset"],
             plugins: ["react-native-web"],
           },
         },
         {
           loader: "awesome-typescript-loader",
           options: {
             configFileName: "tsconfig.json",
           },
         },
       ],
     },
     {
       test: /\.(gif|jpe?g|png|svg)$/,
       use: {
         loader: "url-loader",
         options: {
           name: "[name].[ext]",
           esModule: false,
         },
       },
     },
   ],
 },
 resolve: {
   extensions: [".ts", ".tsx", ".js", ".jsx"],
   alias: {
     "react-native$": "react-native-web",
   },
 },
 externals: {
   react: {
     commonjs: "react",
     commonjs2: "react",
     amd: "React",
     root: "React",
   },
   "react-dom": {
     commonjs: "react-dom",
     commonjs2: "react-dom",
     amd: "ReactDOM",
     root: "ReactDOM",
   },
 },
};
```

First thing we need to define for Webpack is the input file. This is where we export all the components in our library, for example:

_src/index.ts:_

```javascript
import Button from "./components/Button";

export { Button };
```

In the next section, we define what should be the result of the bundling process:


```javascript
output: {
   filename: "bundle.web.js",
   path: path.join(__dirname, "/web/build"),
   library: "our-components-library",
   libraryTarget: "umd",
   umdNamedDefine: true,
 },
```

_Filename_ and _path_ are self–explanatory, but it’s worth remembering that the path to the final bundle and its name must be the same as what’s defined in _web/package.json_, otherwise we won’t be able to import components of the library correctly as they won’t be found. Setting library name and _libraryTarget_ to “umd” and _umdNamedDefine_ allows us to import components from the library individually.

In the module section one needs to define all the loaders used to build particular file types. While _awesome-typescript-loader_ and _url-loader_ probably sound familiar as they are often used with React applications, I’d like to focus on configuration for _babel-loader_. Two settings are worth mentioning here – we need to use _metro-react-native-babel-presets_ to obtain [**Babel**](https://babeljs.io/) presets for our React Native components, and we also add _react-native-web_ Babel plugin. The plugin basically rewrites paths to import only the modules needed by the app. That allows us to vastly reduce the size of the result bundle.

Size concern is also the purpose for _externals_ part of the config:


```javascript
react: {
     commonjs: "react",
     commonjs2: "react",
     amd: "React",
     root: "React",
   },
   "react-dom": {
     commonjs: "react-dom",
     commonjs2: "react-dom",
     amd: "ReactDOM",
     root: "ReactDOM",
   },
 },
```

Thanks to this configuration, libraries mentioned are not bundled into the final file. Without it, we could have the whole React framework bundled and passed between packages. Because React and ReactDOM have to be installed in the parent project of our components library anyway, it wouldn’t make any sense to have it in the bundle.

Finally, we instruct webpack to use React Native Web library whenever we import something from _react-native_. This is obtained with alias configuration:

```javascript
resolve: {
   extensions: [".ts", ".tsx", ".js", ".jsx"],
   alias: {
     "react-native$": "react-native-web",
   },
 },
```
Full list of React Native modules supported by React Native Web is available in it’s [**documentation**](https://github.com/necolas/react-native-web).

After running the _build-react_ script, we obtain a nicely compressed bundle of about 80kB. Thanks to _web/package.json_ configuration, it is enough to import from the bundle like described before:

```javascript
import { Button } from 'our-components-library/web'  
```

## Summary

Module bundlers allow us to develop [**React Native Web**](https://github.com/necolas/react-native-web) and [**React Native**](https://reactnative.dev/) component libraries that can be shared across web and native mobile applications. In the example presented, we used the [**webpack**](https://webpack.js.org/) bundler, but could have used an alternative such as [**rollup**](https://rollupjs.org/).

It’s also worth noting the way presented above is still a bit complex because it requires a lot of manual setup. A vast effort has been recently put into development of React Native and React Native Web libraries, together with an awesome React Native dev environment called [**Expo**](https://expo.io/), which makes building React Native apps even simpler. Taking that into account, we can expect even more examples of cross-platform development in the upcoming years.