One of my current projects involves building much the same thing as both web and native apps. As a result, I have spent a fair bit of time thinking about how best to share code between the different platforms.
Front end logic
This is the easy one - Redux works out of the box with both React and React native. You just need to make sure the actions/reducers don’t directly do anything that won’t be shared. So far that seems to mostly be routing (react native uses a completely different navigation model) and web api calls - even if the app still calls the web api, the authentication process is completely different.
The final version of the app has quite different requirements for working with data - local storage is available, and the whole thing should work offline. Eventually I expect that only the interface connecting it to redux will be shared, but as a first step it turns out to be really useful to be able to use the web api as is. An important part of my development process is being able to use the app myself, and this lets me start using the app before writing any native code. The app isn’t releasable like this, but it feels like the real thing for testing. It also means that only one implementation is needed during early development when stuff is changing a lot.
Although I initially had the API set up as an ES6 module with exported functions, I’ve found the best approach to be a class that can be initialized in the non-shared entry point. It makes little difference for the final implementation, but makes early development much easier - initially only the url and authentication will be different, with more specialized implementations added gradually.
Splitting the API into multiple parts is probably useful here - most likely the app will need to continue connecting to some parts of the api after the core is implemented locally, so make sure it is easy to remove the login / connectivity handling for only half the api calls.
As far as I can tell, UI code is rarely shared between web and React Native apps. On a certain level this makes sense - native components usually look better - but it comes with some disadvantages as well. Building a full set of native components is a significant chunk of work before the app does anything, and keeping all platforms up to date can be challenging in the experimental stage of development.
My approach is to use the page level components from the web version as either final implementation or at least placeholders for the native pages. Redux makes it relatively easy to pipe property updates and actions through the webview bridge. The framework I set up for this, react-native-web-components, is available on github. It’s still somewhat a work in progress and has some limitations, but works well enough for now.
Navigation and page transitions stay as native code and are not shared - this covers 90% of the places where a hybrid app doesn’t look like a real native app, but requires minimal effort when all the pages use the same navbar component. In the past I have built apps using native transitions with cordova, but react-native makes it way easier to mix web and native components.
Differences in the overall app structure are somewhat inevitable anyway - login / connectivity ui in particular is completely different, with the web app requiring authentication on startup, and the native app needing authentication to be optional and only used for some functions.
Routing and app structure seem to be fundamentally unsharable. I am currently using react-router and react-native-router-flux, which are quite different, but I haven’t found any that are significantly more similar and I don’t really want to write my own. Web routes are based on a url for each page in the app, with the option to work out an appropriate transition based on link settings or the current route. Native routes are linked directly to a transition, and are not necessarily valid from all starting routes.
However, in most cases transitioning to a new page requires the same kind of input, so it was reasonably easy to set up a wrapper as part of react-native-web-components which converts a common redux action into an appropriate router action. So far the connection is one way - both routers offer some sort of bidirectional integration with redux, but that hasn’t seemed worthwhile to set up yet.
My current setup involves three main git repos / npm packages:
- app-client - a private npm package which contains most of the react / redux code. It can include some react-native components, but they must be sufficiently separate to not accidentally get imported into a web only package.
- app-web - implements the server side api and hosts the web version of the app.
- app-native - The native app. The only js code is in index.ios.js, which calls most of the same code from app-client but with different settings.
I usually use more pieces than that; I’d prefer to have a separate package for client code that is shared across multiple similar apps. However, the fact that the react packager doesn’t support npm link (or anything involving symlinks) makes that severely annoying. I have a watchman task that calls rsync to copy changes from app-client to app-native/node_modules/app-client, but it’s still annoying. I’m even considering using multiple targets rather than multiple app projects for some similar apps to further reduce the number of pieces.