Combined React (Web) and React Native (Mobile) App

I have a React web app that uses a typical stack (Redux, Redux Thunk) to build a web frontend that consumes a separate API for its data.

I’d like to build a React Native app using Expo that largely reproduces the functionality – reaching out to the same API, etc. It will also use Redux and Redux Thunk, and the hope would be to reuse code between the two projects. The most obvious code to reuse is Redux stuff – actions, reducers – and common utils.

I’m not sure what the best way to do this is.

I had two thoughts:

Shared Package

One thought was to abstract out shared code into a third repo, which I would bundle as an NPM package or pull directly from Github into both apps.

Pros:

  • Clean design, with commonalities extracted out.
  • More explicit separation of concerns and delineation of commonalities.

Cons:

  • Probably a pain to refactor out.
  • Could cause issues on deploy, if I don’t refactor things out perfectly.
  • Likely ongoing headache making changes to the shared directory and syncing them between the frontends.
Nested Apps

Another thought was to create a separate mobile directory in the React app (at root level, next to src) and have that be my React Native app. It would reach back into ../../src/utils etc, grabbing any necessary functions from the main app within which it is contained.

Pros:

  • Relatively easy to set up (just Expo create in the web app directory).
  • Easy to update both apps at once and ensure they both work.

Cons:

  • No idea how this will work in the long run.
  • Could push back any problems until the moment I want to launch on Apple/Play stores.
  • Not sure that this will work with launch.

Is there an accepted pattern for this? Any thoughts?

Thanks,

Sasha

Hi there!

I think the best way to go with this is the second option. You can indeed share some key functions you made with both the web app and mobile app. That is how I do it, for what it is worth.

I’m actually doing #1 :-), though I think #2 could work. In our case, we’re sharing with an Angular app that we want to start integrating React into. Oh, and we’re also sharing the RN bits with another RN app. Too many ingredients for one pot IMO. FWIW, theoretically there is an option #3 of a monorepo, but last time I tried this, I found out Metro Bundler doesn’t support symlinks, making stuff like Lerna a no-go.

@rauldeheer / @llamaluvr – Have you seen a good example of these patterns? Would love to see how others have done it.

Mine’s a private repo (and definitely a work in progress), so can’t share the entire thing, but I think I can describe what’s unique about our implementation of a separate shared repo compared to the typical node module.

The shared library

Everything’s pretty typical here compared to the average React Native library. You’ll want to keep anything like React, RN, Expo, out of your dependencies (since you can only have a single instance of those), but you’ll want things in your devDependencies to facilitate imports, linking, etc.

  "dependencies": {
    "luxon": "^1.2.1"
  },
  "peerDependencies": {
    "react": "^16.3.1"
  },
  "devDependencies": {
    "eslint": "^4.19.1",
    "eslint-config-universe": "^1.0.7",
    "prettier": "^1.13.4",
    "react": "16.3.1",
    "expo": "^27.0.0",
    "react-native": "^0.55.0"
  }

Test examples

In my shared library, I embed both a web and a CRNA project in an /examples folder, (e.g., /examples/web, /examples/native). This should make it easier to test changes without always having to integrate it back into the actual apps. For the native app, I follow React Navigation’s example exactly. See their repo and their example build instructions. I use CRNA here instead of Expo for the ease of avoiding a global dependency.

I did a pretty vanilla webpack/ babel implementation for the web example.

In each example, you refer to your library using a relative path, like:

 "dependencies": {
    "react": "^16.3.1",
    "react-dom": "^16.3.1",
    "common-lib": "link:../.."
  },

Thus, you can run each example, and they’ll both get updates when the library code changes via metro/ webpack hot reloading.

The webpack.config.js for the web example project has some special stuff in the rules. This is because a) I’m leaving my library code in ES6, but it’s typical for web modules to be transpiled down already, and b) I don’t have a .babelrc in the library’s top-level folder (there might be a way to get this to work with a babelrc at each level, but I kept having issues):

rules: [
      {
        test: /\.(js|jsx)$/,
        use: {
          loader: 'babel-loader',
          options: {
            babelrc: false,
            //presets: ['env', 'react', 'stage-2'],
            // weird trick to get babel presets here to work for higher-level folders
            // https://github.com/babel/babel-preset-env/issues/186
            presets: ['babel-preset-env', 'babel-preset-react', 'babel-preset-stage-2'].map(
              require.resolve
            ),
          },
        },
        exclude: /node_modules\/(?!(common-lib)\/).*/,
      },
    ],

So, all this basically tells the example app to transpile the library (but that’s the only thing it transpiles from node_modules) and to transpile it using the rules for the example app.

Worth noting that you don’t need any of this for the native app because native libraries are typically distributed in ES6!

The web project

As I mentioned before, when you import the library into your web project, you need to make sure it gets transpiled by babel. You don’t have to worry about the whole .babelrc outer-folder weirdness, so you can just stick this in your rules for webpack.config.js:

rules: [
      {
        test: /\.(js|jsx)$/,
        use: ['babel-loader'],
        exclude: /node_modules\/(?!(common-lib)\/).*/,
      },
    ],

You could probably avoid this by using rollup or something like that to transpile your library just for web use. I’ve never done that before, and also you’d have to specifically transpile it for web and not transpile it for native (or add it to an ignore on the native side).

Native app

As mentioned before, metro bundler is already set to automatically transpile your node_modules, so just import your library and go.

1 Like

This topic was automatically closed 28 days after the last reply. New replies are no longer allowed.