In this article, we examine a few features of React Native that helped us ship a cross-platform iOS and Android app in 8 weeks, without sacrificing user experience on either platform.

First, we’ll see how code reuse and a fast feedback cycle let us move really quickly and help us rapidly iterate on our designs. Then, we’ll learn about the framework’s flexbox-based layout system and evaluate a few approaches to platform-targeting, for situations where it isn’t practical to reuse code due to varying design patterns or platform constraints.

It’s important not to overlook the data layer of our application as we consider different implementation approaches. One of the biggest benefits of React Native is the unofficial bundling of Redux as a framework for managing and tracking changes in application state. This lets us write one shared data layer that can be used within both our iOS and Android apps, saving us a lot of time and helping us reduce the potential points of failure by only having to maintain our data fetching and state management in one place. This data layer is probably the most critical part of an application to be robust and tested, and having to build that just once helps us ensure its stability across platforms.

Beyond that, Redux is also just a sensible approach to managing application state. Planning and implementing a new feature is easy because Redux clearly defines where many of the necessary pieces of functionality should live. Thus, we can avoid the need for lots of upfront planning and naturally fall into certain conventions and patterns of data fetching and state management. These patterns also serve to ensure the code base is intuitive as we onboard or hand off the project to other developers.

We’ve been quite impressed by how quickly the framework allows us to iterate on mobile UI. This is due in large part to features like hot- and live- reloading, where the application UI is essentially “refreshed” (like a web page) whenever we change files. No more waiting to re-build and deploy our app in order to preview a one-line UI change: we get the immediate feedback loop that we previously only saw when working with web or hybrid apps.

At the same time, these mobile experiences feel truly native, because they are.

React Native’s generic, flexbox-based layout system allows us to write a single component that can be run on both iOS and Android, using plain-old JavaScript objects to define styles. That means we’re able to avoid the need to implement UI separately using two vastly different languages and layout systems. I’ve found this method of styling and layout to be more intuitive than Auto Layout, and it should feel familiar to folks with a background building for the web. That means it’s easy for designers to hop into the codebase and implement their own designs, helping ensure they come out as intended.

This is great because it saves us time, but we should be careful not to strive for complete design parity across iOS and Android. After all, these are two different platforms with varying design patterns and visual direction.

Users of each platform simply have different expectations when it comes to mobile interfaces and experiences. We want to ensure that every user is served the most familiar, intuitive experience for their platform, and that means implementing platform-specific design and styling in our UI components.

There are two primary approaches to writing platform-specific code, and we can take advantage of each in interesting ways. Let’s take a look at them now.

React Native exposes the Platform module, which detects the platform on which the app is currently running. It exposes a property called OS which allows us to control the flow of our application based on the current device platform. As an example, let’s suppose we want to tell the user which company made the OS they’re running, just in case they’re not sure. We can do something like this:

import { Platform } from ' react-native ' if ( Platfom . OS === ' android ' ) { alert ( ' Your OS is made by Google ' ) } else { alert ( ' Your OS is made by Apple ' ) }

This works pretty well but there’s still a bit of duplication we’d like to avoid. Sometimes we may need to diverge our application flow by platform in order to perform the appropriate action, but often it’s as simple as swapping some values around based on the current platform. In that case, we can simplify the code above using a nifty Platform method called select . It takes an object whose keys correspond to the available platforms and returns the appropriate value for the current platform. Let’s see how that looks:

import { Platform } from ' react-native ' const company = Platform . select ({ ios : ' Apple ' , android : ' Google ' , }) alert ( `Your OS is made by ${ company } ` )

This method can accept any value, including functions, so we can do some pretty powerful stuff with it. It’s easy to see how this approach can be valuable for supplying different appearance values based on the platform or in any situation where the difference in implementation between the two platforms is rather small.

I want to emphasize that last point. We should strive for as much code-reuse as is realistically possible, but not at the expense of clarity to the developer or to the user. The goal here is to write programs that are easy to maintain and work well. Littering our components with too many Platform expressions and conditionals is antithetical to that goal because those divergences will inevitably make the program more difficult to understand (and thus maintain).

So what do we do when our implementations are very different for each platform? Fortunately, React Native’s packager has a clever solution to that, which comes in the form of platform-specific file extensions.

When we write a statement like import Button from './button' , the packager looks in the current directory for either a file called button.js (or a directory called button/ containing an index.js ) and bundles it appropriately. Platform-specific extensions allow us to write a separate file for each platform, e.g. button.ios.js and button.android.js . The packager will then bundle the appropriate file that corresponds to the packager’s current target platform.

├── index.js // cross-platform ├── button.ios.js // iOS-specific └── button.android.js // Android-specific

Let’s see how we can take advantage of this to build a generic component that requires different, platform-specific implementations. Let’s suppose no cross- platform <Switch> component exists, and we want to write our own:

// switch.ios.js import React , { Component } from ' react ' import { SwitchIOS } from ' react-native ' const Switch = ({ disabled , onChange , style , value }) => ( < SwitchIOS disabled = { disabled } onValueChange = { onChange } style = { style } value = { value } / > ) export default Switch

// switch.android.js import React , { Component } from ' react ' import { SwitchAndroid } from ' react-native ' const Switch = ({ disabled , onChange , style , value }) => ( < SwitchAndroid disabled = { disabled } onValueChange = { onChange } style = { style } value = { value } / > ) export default Switch

This is a contrived example, but we may encounter situations which require vastly different implementations, especially when working with native modules. How about another example. Let’s suppose now that we want to write a <SwitchField> which will contain a switch and a label. Now that we have a generic <Switch> , we can probably stick to one generic implementation, but we still want platform-specific styles. We’ll ultimately have three files:

├── switch-field.js // cross-platform component ├── switch-field-style.ios.js // iOS-specific styles └── switch-field-style.android.js // Android-specific styles

// switch-field.js import React , { Component } from ' react ' import { Text , View } from ' react-native ' import Switch from ' ./switch ' import styles from ' ./switch-field-style ' // Imports platform-specific style const SwitchField = ({ style , label , onChange , value }) => ( < View style = {[ styles . container , style ]} > < Text style = { styles . label } > { label } < /Text > < Switch style = { styles . switch } onChange = { onChange } value = { value } / > < /View > ) export default SwitchField

Now we have our generic <SwitchField> , we’d like to specialize the style with two independent switch-field-style files. Let’s see how that might look:

// switch-field-style.ios.js import { StyleSheet } from ' react-native ' export default StyleSheet . create ({ container : { alignItems : ' center ' , alignSelf : ' stretch ' , flex : 0 , flexDirection : ' row ' , justifyContent : ' space-between ' , }, label : { alignSelf : ' flex-start ' , fontSize : 14 , }, switch : { alignSelf : ' flex-end ' , }, })

// switch-field-style.android.js import { StyleSheet } from ' react-native ' export default StyleSheet . create ({ container : { alignItems : ' flex-start ' , flex : 0 , flexDirection : ' column ' , }, label : { fontSize : 14 , marginBottom : 10 , }, switch : { marginBottom : 10 , }, })

As we have it, our version of <SwitchField> on Android will show a stacked label and switch control, but on iOS we see a label on the left and a switch control on the right.

This is great, because we can keep our component logic in one place but maintain our platform styles independently. If we decide to make a change to the way our <SwitchField> looks on Android, we can be sure it won’t affect its appearance on iOS.

It’s also worth noting that, while these examples pertain to Components, both approaches are by no means component-specific! You can use either of these solutions (the Platform module or platform-specific file extensions) in any module within your React Native application.

In the end, React Native has been great for building cross-platform apps but we still must be careful about how we approach cross-platform functionality and styling. Hopefully the examples help you understand the differences between these approaches so you can make the right decision when you find yourself needing to target a particular platform in your React Native apps.

Thanks for reading!