I picked up React Native as a web developer and here's what I've learned
Laura buns
Posted on June 9, 2019
For the last couple weeks, I've been building a React native app at work. It's a news reader (duh) and a bit of a monster at that, with filesystem access, background downloads, and push notifications.
This wasn't my first time using React but!! It was my first time using RN. Which is scary because new things are scary. It's been a great experience though, and I'm kinda waiting for an 'OH S**T' moment where something unexpected goes horribly wrong – but so far it's been ridiculously fun.
Why React native? Well, my team originally wanted a web app! (a PWA, they are all the rage now) We changed course for three key reasons:
- Despite web being a 'nice to have' our first market was app stores
- We wanted it to have very elaborate offline & background functionality. This is very early & experimental on web but a solved issue on mobile apps since day one.
- We wanted to deliver a native-like experience. Think 60fps animations, multiple stacked views, the works. These are solved problems in the app world too but on the web we are on our own.
- With
react-native-web
we have a path to turn this back into a PWA if needed
It's not the web
On the web, plain React eventually generates an HTML-based website. This is how you can use CSS and directly call DOM functions on your components.
Native is a bit of a different beast. Despite using React's syntax – and unlike libraries like Cordova – RN never gives you HTML, or DOM Elements or CSS, but rather orchestrates native views directly on your mobile OS. This is pretty awesome because it means your UI is truly native. Sure, it's being assembled on the fly using javascript but it's using the same set of blocks the rest of apps are using.
How different is this from standard React? Not a lot to be honest. The primitives are pretty similar!
/*react web*/
const Counter () => (
<div className='row'>
<button onClick={setCount(c=>c+1)}>Add number</button>
<span>{count}</span>
</div>
)
/*react native*/
const Counter () => (
<View style={styles.row}>
<Button onClick={setCount(c=>c+1)}>Add number</Button>
<Text>{count}</Text>
</View>
)
Using native UI not only makes your app a better citizen but also it's, like, fast. If you are used to struggling to get 60 fps animations on the web this is a whole new world where you just get that. For free! even on old as heck devices! (More on performance in a second part)
By the way! You don't get all the semantic element niceness from HTML5 in here either. Almost everything in RN is a View
. This means it's super important to mark up the semantic purpose of your views for a11y purposes. You can use accessibilityRole
to do that. If you need alt text, accessibilityLabel
has you covered.
Getting started
I had some incredibly basic Xcode experience from doing prototypes eons ago (back then xcode looked like itunes? it was a weird time) but anyway I kinda knew what to expect compared to the web – faster apps, but a slower dev cycle with harder to use devtools.
_
i
was
so
wrong
_
First of all, if you just wanna dip your toes in the native waters you don't need any of this, you can use expo to run your javascript and handle all the app-y bits. This gives you significantly less control over the app bits on your app but what's pretty cool is that all your code is still vanilla React. If you ever need that control you can just expo eject
at any point and get your raw Xcode and android studio projects.
Even after you eject, you still won't be using Xcode or Android studio for the most part (unless you wanna). react-native run-ios
will fire up a simulated iPhone X and run your app, and react-native run-android
will install it directly to your phone that you only meant to charge but it's fine I guess now you've got an app on your phone.
The react docs on setting up Android Studio are pretty good. When it comes to iOS, code signing your app is a bit of a pain – you need to do this before running it on a iOS device. You don't need to be a paid member of the apple developer program to do this but you need to be signed in into Xcode. What i normally do is try to compile it, click on everything red, and click the 'Fix issue' buttons until there are no more issues.
Finally, when running your app you can shake your device or simulator to get a pretty cool debug menu. You can hot reload code just like on the web, run the chrome devtools to hunt for bugs, or even open up the worlds cutest little inspector:
Styling
You will probably want to style your app. Unless you are making a todo list or whatever you will probably want to style your app a lot.
React native comes with a built in StyleSheet
module. it handles styling for you. This rules because you don't have to argue ever again about what css-in-js solution to use. It's also bad because StyleSheet
is so similar to CSS you might think you are writing CSS but the similarities are only surface deep.
const styles = StyleSheet.create({
button: {
borderRadius: 999,
backgroundColor: 'tomato',
padding: 10,
paddingHorizontal: 10,
},
text: {
textTransform: 'uppercase',
},
})
const Button = ({ children, ...props }) => {
return (
<Touchable {...props}>
<View style={styles.button}>
<Text style={styles.text}>{children}</Text>
</View>
</Touchable>
)
}
The built in documentation on how to style things is very good but I wanna get into the big changes first
It's pretty much like css-in-js
Your styles are a javascript object with camelcase properties. If you have used emotion
or styled-components
you'll feel right at home with this way of working
Chonky pixels
Most phone screens are pretty dense and scale up their UI so, as a unit, 1px
is a lot and pretty big looking for borders. You can use StyleSheet.hairlineWidth
to get the size of 1 screen pixel across devices.
But everything is a flexbox
Since all StyleSheet
does is talk to the underlying OS you are restricted in ways you can layout compared to CSS. If you want to float something (For example to wrap an image to the side of some text) you are completely out of luck. Same goes for using CSS grid!
You have a magical flex
property that consolidates flexGrow
, flexShrink
and flexBasis
into a single number. I have no idea how to use this. @NikkitaFTW calls it 'backwards flex'. She has no idea how to use it either.
So you can't float things
Ours is quite a special case but since our app had to render very type-heavy articles. To fix this we decided to render the body of the article in a webview and put that inside our React native app. This felt wrong and counter intuitive since "it's all javascript anyway" but it's important to always use the best tool for the job and the web was built to render documents!
Or debug layouts 😰
Remember when you had to start coloring divs red to see where your layout had issues? Get ready for some NOSTALGIA. RN does offer a built in inspector but because it's inside the simulator (or inside your phone) it's kind of a hassle to use.
And there's no cascade or selectors
You apply your styles directly to your components. You can't style children based on their type or have things like hover
or disabled
states or :before / :after
pseuds.
This sounds super limiting but in reality having a well architected and modular app with small components will take care of a lot of this for you.
None of your styles cascade, this can make your CSS more predictable but also a bit of a pain. We remediated this by using react context to encapsulate style properties we wanted to cascade down like theme colors. Context is ideal for this because you can have multiple contexts in the same screen for different nodes, almost working like css variables.
This is a bit of an oversimplification (we've got a useAppearance() hook that returns the values directly) but you get the idea:
/*
in your appearance file
*/
export const appearances = {
dark: {
backgroundColor:'#000',
color: '#fff',
},
light: {
backgroundColor:'#fff',
color: '#000',
},
}
export const AppearanceContext = createContext('light') // <- that's the default!
/*
in your view
*/
<AppearanceContext.Provider value={'dark'}>
<Button>I'm dark!</Button>
</AppearanceContext.Provider>
<AppearanceContext.Provider value={'light'}>
<Button>I'm light!</Button>
</AppearanceContext.Provider>
/*
in your component
*/
(...) => {
const { backgroundColor, color } = appearances[useContext(AppearanceContext)]
return (
<View style={{backgroundColor, color}}>{children}</View>
)
}
The loss of the cascade is not that big of a deal as it might seem except for a single but very important use case:
Text
All text you want to render in React native has to be <Text>Wrapped in a text tag</Text>
and it will display in the system font at 16px.
You can of course style your text to have any font and size you wanna, but text comes so many shapes and sizes that you should be prepared to have a ton of variations. In our app we ended up having a single file for all our styled text elements but I'm not sure this is the best structure.
When it comes to fonts you'll probably wanna use custom fonts! Especially now that all apps are white on black with a bunch of lines and there is literally no other way than type to tell them apart. Good news first, you don't have to deal with @font-face
rules which is pretty neat!
Sadly everything else is pain. Your fonts will live duplicated inside your Android and iOS projects and here's where it gets hairy: To use a font in Android you will reference its filename, to use it on iOS you will reference its Postscript name. Don't know what that is? Don't worry, I didn't either. It's this thing:
Images & icons
If you are following modern design trends most of your images by now will be flat vector images, probably inline SVGs and boy do I have bad news for you: You don't get to use normal SVGs in React native. They are not supported by the <Image/>
element. This is bad especially for icons and such. How do you load images then? There's a couple strategies:
For complex shapes and such you can convert them into bitmaps, 90s style. You'll probably wanna set up a build pipeline to churn them out for you. All the assets in your app will be downloaded upfront so file size is not as big of a critical consideration as it is on web (but don't go bananas!) To make sure bitmaps are crispy you will want to export them at @3x
their intended size on screen.
If you want to remotely import SVG that's a bit trickier but not impossible! There are several libraries that will do this for you by essentially chucking them in a webview.
For everything else (I'm doing this!) You can use react-native svg
to use SVGs inside your code. The way this works is it exports React native versions of everything in an svg and you can use these and it draws the proper views for you
Having SVGs be first class citizens in React with props and animation and everything has changed the way i see all SVGs. i always knew they were markup but having to directly adjust them myself now has given me lots of ideas for cool things I can do with them.
At the end of the day react-native svg
is a very elaborate hack that gives you views so it can also be used as a low level drawing library for things like lines and circles and whatnot! Your imagination is the limit!
A good way to assess what image loading strategy to use is by asking yourself how messed up will things be if this doesn't load? so for example you might want icons to be inline SVGs but big hero images to be remotely downloaded. Be aware that some things will always be messed up and that some of your users will never see images anyway because they use screen readers or have poor eyesight or they just can't figure out what an arrow coming out of a box in a circle is supposed to mean.
Always make sure you have proper accessible descriptors for all your images! And provide sensible fallbacks if an image can't load (For example, in a hero, code in a background color that gives the text enough contrast)
Navigation
react-navigation
kinda sounds like the react-router
of this land. You might have noticed that mobile apps have more advanced navigation types than the web. You can't just replace things in place and call it a div, if you look at any mobile app, all your screens slide out and in and away. react-navigation
has a data model that is super linked to these transitions.
Each navigator is a flat list of screens with an entry point and each defines the transitions between its screens. For example you can use a single navigator for all your app and all your screens within it will do that thing where they progressively pile on top of each other from left to right.
export const RootNavigator = createAppContainer(
createStackNavigator({
Main: HomeScreen,
Downloads: DownloadScreen,
Settings: SettingsScreen,
})
)
But say you are doing a music player and you wanna add a card that can slide over any views with some "now playing" info. You can just create a new top level navigator that contains your original navigator and that lonesome card. You can even just use {mode: 'modal'}
on it to get a pre made animation and voila, now if you navigate to your now playing view it glides over the rest of your app!
export const RootNavigator = createAppContainer(
createStackNavigator({
Main: createStackNavigator({
Main: HomeScreen,
Downloads: DownloadScreen,
Settings: SettingsScreen,
}),
NowPlaying: NowPlayingScreen,
},
{
mode: 'modal'
}
)
Something really cool is that even though your navigators are in a hierarchy your route names aren't. You can navigate from any route to any route without worrying about reaching out to the top level or something. It just works™.
For accessibility reasons you will probably want to use <Link />
like this. This will make things neat and tidy if you ever make a website with react-native-web
Good to know! react-navigation
gives you a lot of control but in exchange it recreates a lot of the platform's native navigation views. If you have simpler needs you might wanna look at react-native-navigation
which implements the platform native navigation bars at the cost of flexibility.
To sum up
The only bad thing I can say about React native is that it's too good? As I said at first I'm still waiting for a major 'oh no' type moment where I rode a wrong assumption for far too long and half of the app is broken or something.
Funnily enough this happened with my first React (web) app! We got a last minute requirement to make it work on Samsung Internet on low end phones and well, it was a Redux and websocket fueled beast, best we could do was getting it to crash at the signin screen instead of at the splash page.
IMO RN is pretty good and I feel sometimes it can get a bit of unfair flak. Web developers fear it because it's not the web and app developers fear it because it's an unnecessary abstraction. Personally I'm hella impressed at how elegant it is as a solution to write multi platform apps that feel like they belong on each platform. I'm also super excited about eventually using react-native-web
to go full circle and get a PWA!
🥳
Crossing fingers this was interesting to read! There are parts of this that I feel I could turn into a full blown book!! I'd love to hear your thoughts on what you found odd or funny in React native and I hope this post inspires you to start making apps!
Did you enjoy this post? Pls let me know! I wanna publish a followup with even more things like animation and perf but I don't wanna bore the world with my React native ramblings.
psss. you can follow me on twitter @freezydorito
Posted on June 9, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.