I learned React Native as a web developer, and I got everything wrong

November 26, 2024
views

When I built my first React Native app, I had some prior web experience. Using React on iOS and Android felt like a natural way to apply my skills.

But I was surprised to learn, the hard way, that my web-developer-way-of-thinking didn't apply to native apps.

To understand why, let's start with the navigation layout. Each website has unique page primitives. The header at the top of the page, the sidebar menu, the footers — they're all hand-rolled.

You write a <header> and all you get is an empty box. You then use your own JavaScript and CSS to make it useful.

The way these elements look and feel is part of your brand. If your header looks like another website, something feels off.

When I started building native apps, I brought this instinct with me.

As I built a screen, I created a custom header component. I laid out the back button and header title. I made my own animation interpolations between screens. I made my own bottom tabs. I managed the safe area myself. I tracked the screen orientation and animated the transition accordingly.

I couldn't let my app look like everyone else's.

I would soon learn that making my low-level UI primitives from scratch was a big mistake.

Users expect similar patterns from all apps. And they want a certain level of quality from their navigation primitives. If you can't long press your iOS back button to go back a few screens, it feels weird. Small details compound — especially the bad ones.

Why remake a custom header when the native one works perfectly? Because that's what I learned from web. Turns out, I should have just used the native header and customized its colors.

Better yet — add zero customization and just use UIKit components. Rather than tell you your app feels basic, people will applaud how much it feels like it belongs on an iPhone.

Why did so many people prefer Apollo over the actual Reddit app? Because it embraced the iOS look and feel.

The web, on the other hand, is the wild west. Take a blank white canvas, and make everything yourself. The built-in primitives are ugly. You need a CSS reset to keep browsers from using Times New Roman. Buttons are gray. Inputs have an ugly blue outline and zoom on mobile. Clicked links turn purple.

UI kits for the web come and go. Libraries spark up, free and paid, to help you avoid the harsh journey of building a button.

Can you imagine if low-level html primitives looked good?

The greatest surprise to me, in retrospect, is how beautiful, cohesive, and opinionated the iOS design primitives are. Navigation shells, animations, search bars, menus, typography, colors, icons, and more. You have a design system made by some of the world's greatest digital artists, tailored to the platform they built. Decades of caring about flawless experiences are baked into a language with strict guidelines.

It took me too long to appreciate this fact. I didn't even think to read the Human Interface Guidelines.

In 2019, React Native apps felt a bit like Flutter apps do today. They matched the look of the underlying platform, but not the feel. Like a high-quality knock off.

Sure, the React Native philosophy has always been to match underlying platform. But in reality, React Native UI libraries rarely used native UI primitives. Instead, they used JS-based implementations that looked like the platform.

On Android, React Native apps used Material Design headers on their pages, but they were implemented with JS. They weren't actually using the native header component. There was always something a little different.

To people like me, with a background in web development, this abstraction was ideal. I get full customization over the header. Why would I want anything less?

It's hard to blame React Native developers. After all, React Native made it really hard to use native code. And even when you did, you likely couldn't use it with Expo. If you wanted to use a native context menu, you'd have to uproot your entire stack and go with bare React Native. Quite hard to justify.

Libraries with native code would often become outdated, maintainers would lose interest in dealing with React Native's config issues between upgrades, and consumers would have no clue how to edit the Objective-C files to patch the library. "JS-based" even became a mini buzzword in library READMEs, boasting the fact that there was no risk of dealing with pesky native dependencies.

This tension between JS and native kept my web developer mindset going. Clearly JS-first was the way to go, right?

A lot has changed in the past few years. You can now use Swift and Kotlin to build native modules for React Native with far less config. Perhaps best of all, native code finally works with Expo.

Although it's been slow, these architectural changes have had an impact on the React Native ecosystem. There is a growing embrace of Native-first development. Libraries like React Navigation have changed their UI to use native primitives, prioritizing a native user experience over developer customization.

It's my hope that this trend continues, and that using native code becomes as easy as import Element from './file.swift'.

So avoid the mistakes I made, and use the platform.

And above all, know the rules before you break them.

I wrote the first draft of this post in June 2023, but I forgot to publish it at the time.