In the spring of last year, Curai acquired First Opinion (now Curai Health), a service that enables users to chat with a physician and get answers to their medical questions, 24/7. Users can chat in a mobile app or on the web. With the acquisition we gained great new teammates and a promising product; and, as is often the case, we inherited a legacy codebase. We quickly decided to port our frontend First Opinion code from backbone.js (web) and Objective-C (iOS) to React/React Native, and a few weeks into the process decided to give React Native For Web a try. For the unfamiliar, React Native For Web is an open-source library that enables you to run React Native components and APIs on the web. At Curai, we decided to use React Native Web in order to code share across web and native, and while there have been challenges — it worked!
Why React Native For Web
At Curai, the functionality on our First Opinion iOS app is nearly identical to what users encounter on web. When we first began porting to React, we found ourselves duplicating quite a lot of code. For example, a core part of our application is the chat window for doctors and users to chat back and forth. We initially had a series of Chat related components duplicated across native and web. Web components had web-based JSX markup — divs, spans, paragraphs etc. In contrast, our native components used (surprise, surprise!) React Native primitives — Views, Texts, and Touchables. At the same time, nearly all of the core functionality and logic within our app was duplicated. This pattern made React Native Web (RNW) an opportunity for us.
There are certainly pros and cons to consider when deciding if RNW is for you. Early on, we were still getting comfortable with and learning React Native, and we were cautious about becoming dependent on a relatively new external library. We are a small team, and debated whether the benefit of code sharing would be worth the effort spent combining components. React Native Web is not yet compatible with every React Native primitive (although the chart here provides a handy way to check compatibility quickly!). As we made our decision, we found relatively large players, including Twitter and Uber were using RNW in large scale production applications. We also saw that RNW promises fantastic accessibility support and that Nicolas Gallagher, RNW’s creator, is actively maintaining the library. The allure of code sharing and building our web and native applications in a language common across our engineering team was strong — so we decided to give RNW a try, starting with a small new feature we were building.
For those who are interested in a practical how-to for getting started with RNW, the library docs are instructive. At Curai, our journey started with our customer facing web app where we yarn installed react-native-web and updated our webpack and babel configurations. With that, we crossed our fingers for it to “just work out-of-the-box” as promised and were pleasantly surprised. This out-of-the-box magic is powered by React Native Web’s ability to take a generic React Native component e.g. a View and transpile it to a DOM representation, e.g. a div.
The above is sample code from our first feature that used RNW in production. As you may notice, the component uses React Native Primitives that we’re importing from React Native directly. The difference is now we are using the button panel platform agnostically. Our first foray into using RNW in production, with a small new feature, went relatively smoothly. For Curai, the next step was to start integrating components we had already built.
At Curai, we house our web and native directories in a shared repository alongside both a web app that our physicians use to chat to our users, and a directory called common, where components shared across platforms live. We decided on this setup to enable easy configuration sharing and versioning . This means that as we share more and more code through our use of RNW, the number of components housed in the common directory grows. We decided not to spend dedicated sprints combining web and native components, instead tackling it at points when repetitive code became too odious or new features called for it. The bulk of our core app functionality now lives in shared components — our web app uses 75% shared code, and our native app 60%.
During the holiday season, we saw a slow-down in performance of the app, with doctors reporting excessively slow load times. There were a few reasons for this, and our use of RNW was certainly not the cause. However, in our move towards RNW we had neglected to move away from inline styles in favor of React Native’s StyleSheet. While the performance benefit gained from using StyleSheets in React Native is negligible (if it is present at all), there is a significant performance benefit gained from using StyleSheets in React Native primitives rendered via RNW. The performance gains are clearly demonstrated here. StyleSheet is faster because React Native Web turns all declarations into pre-compiled CSS. After switching to StyleSheet, we saw a substantial performance benefit even though we continued to include some conditional styling directly in-line.
Transition Components Thoughtfully
Components should share code when doing so enables code reuse and facilitates continuing to manage state in a sensible way. At Curai, we manage the bulk of our state locally within components. As we transitioned more complex components or components with many children towards code sharing — we noticed a tempting anti-pattern. In order to render platform specific components as children of a shared component, an option was to pass the components down as props. However, this would have led to component specific state management to live in far-off components. Our solution for this was usually to move components in chunks into our common directory. For example, our Header component was very similar on native and web, but its child, the Menu, was really different. Even so, we decided to move both to RNW at once because these components’ state management and functionality was so closely tied.
Another consideration when deciding when to move code to a shared component is how many platform specific features is one too many. Initially we saw a few platform specific features — how photos are uploaded for example — and hesitated to move code to share. If it’s just a few chunks of code though, platform specific rendering is your friend! Another option is to inject platform specific code via props which has the added benefit of enabling testing components in isolation. Along the way we also learned that components like SafeAreaView that initially seem very iOS specific can be rendered on web with no side-effects. Lastly, it goes without saying that code reuse does not always lend itself to moving faster, which adds an additional layer of thoughtfulness required before deciding to jump in with RNW.
You May Need Fewer Higher Order Components
Prior to using RNW, we used Higher Order Components (HOCs) in order to share some logic between our native and web components. For example, initially we had quite a few wrapper components that conditionally rendered either a div or a View depending on the platform. We no longer needed any of these wrappers once we started using React Native Web.
Don’t Mix Your Metaphors
Along the way we hit a few gnarly bugs, and many were caused by mixing our primitives. In one case, a click event was working inconsistently. We had wrapped RNW primitives inside of a div with overflow-y set to scroll. When we replaced the div with a ScrollView, it fixed our problems.
Our hope is that React Native For Web is here to stay. The ability to render UI using platform-agnostic primitives is powerful. Now, instead of thinking about code in platform-specific terms, we are freed to focus on the core functionality when designing and building applications. Though our transition to code sharing through React Native Web was slow, it enabled us to share significant blocks of code which we hope will enable us to move more quickly in the future. With hooks on the horizon offering an additional mechanism to share functionality across components, we are also curious to see how that option will affect the benefits of RNW.
If Curai’s product engineering work interests you, please check out our job page. To continue the conversation, you can also find me on Twitter. Lastly, massive gratitude for Nicolas Gallagher for his work creating, improving, and maintaining the React Native For Web library.
Many thanks to Vignesh Venkataraman for edits and collaboration on this post.