Sharing a codebase across platforms
4 September 2020
Recently I was working on a cross-plattform app built with React and React Native. The goal was to develop a web app and two native apps based on the same codebase. This article is not a tutorial about any of the tools used. Instead I want to explain the main difference between React and React Native, why a third tool called React Native for Web is needed, and what might be considered before starting a project like that.
If you want to learn more about the project in general, check out my overview article.
Table of contents
Differences between React and React Native
Before I started the project, one misconception in my company was that we could use React Native to automatically compile a native app from our React code. The idea was that we can just start writing a web app as usual and later in the process we could build a native app directly from that codebase. That is not the case.
React Native does use the same concepts like React: You write JavaScript components, you have the same lifecycle methods, you have props and state, you can write JSX in your render function etc.
But to render platform-specific UI, React Native uses completely different elements (things like <Text>
, <View>
or <TouchableOpacity>
instead of <p>
, <div>
or <button>
), which is a fundamental difference. Also we cannot write CSS as we know it, but we need to use JavaScript syntax for styling.
Does that mean we cannot use the same codebase for both the web app and the native apps?
No, it does not. And that's where React Native for Web (RNW) comes into play.
React Native for Web
React Native for Web is a useful library to use React Native components on the web. So a React Native element like <View>
would be rendered as a standard HTML <div>
element. That way we are able to share components between both systems. RNW covers a lot of components, but not all. A list of components can be found in the documentation.
Shared Components
Sharing components was the main reason why we decided to write a cross-platform app in the first place. We wanted to write them only once. Unfortunately this is too good to be true. Not all components and APIs are available on all platforms in the same way.
Our first approach to this problem was to separate our components into a web, a native and a shared folder, using symlinks to share common resources like the package.json and node_modules. We would then import the components we need in both apps from the shared folder.
Unfortunately this turned out to get pretty messy over time, because this only works as long as an imported shared component doesn't have any child components that need to import platform specific modules.
For example, when a (shared) LearnElement
component imports a Video
component that needs to differentiate between a native and a web version, we also need to separate the LearnElement
again. Otherwise the shared component would have to import native modules, which the web doesn't support, and therefore would throw an error.
But there is a better solution. When we have components that we need to differentiate, we can simply split them into two and add a special file suffix, like this:
- video.js
- video.native.js
Now we are able to import Video from './components/video'
and React Native automatically takes the appropriate file depending on the platform. You could even split different native platforms like this:
- video.js
- video.android.js
- video.ios.js
That way we are able to cleanly seperate platform-specific components. It doesn't matter what child components the native version needs to import, because we never try to build the web app with this component.
Another way of seperating platform specific code is to use the Platform
module. It lets you define variables per platform like this:
import {Platform} from 'react-native';
const label = Platform.select({
ios: 'iOS',
android: 'Android',
web: 'Web'
});
You could also ask for a specific platform like this:
if (Platform.OS === 'ios') {
// do something specific to iOS
}
In general my suggestion would be to make the components as small as possible. When a component has a platform-specific implementation, make use of the file suffix. When you need to handle code for multiple platforms in the same place (and don't need to import platform-specific modules for that), use Platform
.
Caveats
Semantics
One thing to be aware of when working with RNW is that elements are mostly rendered as simple div
s. Which means that by default we have hardly any semantics in our rendered HTML.
There are special attributes for assigning semantics to an element. However, you have to take care about that. For example, in order to make a heading render as a "real" heading, we need to set the appropriate role and aria-level to our text element.
React Native:
<View>
<Text accessibilityRole="heading" aria-level="2">Course Title</Text>
</View>
HTML Output:
<div>
<div role="heading" aria-level="2">Course Title</div>
</div>
Testing
Another problem - that relates to the previous point - were our acceptance tests. We wrote our tests using Codeception and Gherkin, where you can write things like this:
WHEN I am on the Dashboard
AND I click on "Continue course"
THEN I see "Chapter 2"
The problem is the second line: Codeception is looking for a clickable element (such as a button or a link) when calling the I click on...
method, but "Continue course" is only a div
in our markup. Therefore it throws an error saying that such an element cannot be found on the page.
Our workaround was to add a data-attribute to every element we want to test and then use a CSS selector with the following syntax:
WHEN I am on the Dashboard
AND I click on element "[data-test-id="continue-course"]"
THEN I see "Chapter 2"
This works perfectly fine, even though it is a bit more verbose.
Summary
You don't get a native app with your React app for free, but React Native for Web makes it quite easy to get multiple apps out of one codebase. I wouldn't say reusing code is always better at any cost, because sometimes it adds unnecessary dependencies between components, makes the code more coupled and therefore less maintainable.
But if you have a project with lots of similarities and just a few platform-specific differences, it can certainly save time and effort compared to developing three seperate apps for each platform. Before you start something like that, I recommend taking the time to have a clear understanding about those differences and think about a reasonable component structure before you start. Of course this is valid for every project, but even more so when developing cross-platform.
Thank you for reading. If you have any questions, I am happy to answer them.
I have written more about React Native for Web: