Published

- 7 min read

React Native and the Stockholm Syndrome

img of React Native and the Stockholm Syndrome

The Hubris of the Systems Programmer

There is a distinct cycle that developers go through when they spend too much time writing systems-level code. After months of wrestling with the borrow checker in Rust or carefully managing memory in C, you develop a sort of elitism. You look at the web ecosystem—with its tens of thousands of npm packages and seemingly infinite layers of abstraction—and you think, “I am above this. I have escaped the madness.”

I really thought I had escaped.

But then I decided to build Prior. Prior is a predictive modeling and leaderboard tracking application. At its core, it’s a very computationally intensive, math-heavy project. The kind of thing you’d build a CLI for and run on a Linux server. The problem? Nobody uses a CLI for their daily habit tracking or predictions on the go. If I wanted people (including myself) to actually use it, it needed to be a mobile app. And just like that, I was back.

The Illusion of Choice

So, I had to pick a mobile framework. I evaluated the options with the skepticism of someone who has been burned before.

Swift and Kotlin are the “right” way. But I am one person, and I do not have the time or the emotional bandwidth to write the same application twice. I also do not have the patience to learn Android’s Gradle build system, which I am convinced is a form of ancient dark magic designed by people who genuinely do not like other people. The documentation reads like an incantation, and the build errors are written in a dialect of English that I am not fluent in.

Flutter is Google’s UI toolkit. It’s written in Dart, a language that feels like Java and Javascript had a very boring child who grew up to be an accountant. The UI is painted directly onto a Skia canvas, which is cool until you need to integrate a native library and suddenly you’re bridging to C++ through platform channels. Also, I am not sure I trust Google to keep any project alive for more than three years (RIP Google Reader, you beautiful angel).

React Native promises “Learn once, write anywhere.” The reality is closer to “Learn once, debug native bridging issues on two different platforms while questioning every decision that led you to this moment.” But it has the largest community, the most third-party libraries, and crucially, it uses JavaScript—a language I already know and have a complicated, emotionally exhausting relationship with.

Against my better judgment, I chose React Native. Specifically, I chose the Expo ecosystem, because everyone on the internet told me it was the way to go, and if I can’t trust strangers on the internet, who can I trust?

”It’s Just React”

This is the single biggest lie in modern software engineering.

When you start, it does feel like React. You write some JSX, you use some hooks, you map over an array to render a list. “Wow,” you think, adjusting your imaginary programming glasses, “I’m a mobile developer!” You feel proud. You text your friends a screenshot of a button that says “Hello World” rendered on a phone simulator. They do not respond.

Then you try to add something real. A map. A camera. A persistent local database. Or, god forbid, a custom native module.

Suddenly, you are no longer writing Javascript. You are configuring Podfiles. You are editing MainApplication.java. You are staring at Xcode build logs that are forty thousand lines long and reading errors about missing headers that were definitely there five minutes ago. You are googling error messages that return exactly one Stack Overflow result from 2019, and the accepted answer is “I fixed it by deleting my node_modules and running pod install three times.” You realize that React Native is not an abstraction over iOS and Android; it is a translucent window that shatters the moment you lean on it too hard.

I spent an entire weekend trying to get a splash screen to display correctly on both platforms simultaneously. On iOS, it was too large. On Android, it was the wrong color. When I fixed Android, iOS broke. When I fixed iOS, the app crashed on launch. I eventually solved it by accident while trying to fix something else entirely. I still don’t know what I did. I committed the code immediately and did not touch it again.

Expo EAS: The Saving Grace (Sort Of)

If there is one thing that has prevented me from throwing my laptop out of a moving vehicle, it is Expo Application Services.

EAS is essentially a cloud service that says, “Hey, we know that compiling mobile apps is a terrible, soul-crushing experience that no human should endure. Send us your code, and we’ll do it on our servers. Go outside. Touch grass. We’ve got this.”

And it works shockingly well. I can push a commit, run eas build, and go make a coffee. When I come back, I have a binary ready for the App Store. It handles the signing certificates, the provisioning profiles, and all the bureaucratic nonsense that Apple demands before they let your code run on their precious glass rectangles. It even handles the over-the-air updates, so I can push a bug fix without going through the App Store review process (which takes longer than it took to write the bug in the first place).

But deep down, I know the truth. Underneath the sleek EAS CLI, there is a Mac Mini in a server farm somewhere, running npm install, downloading half the internet into a node_modules folder that weighs more than the Mac Mini itself, and compiling Xcode projects with all the grace and speed of a glacier. The complexity hasn’t disappeared; it has merely been outsourced to a server that I am paying $5/month to suffer on my behalf.

The Bridge Tax

There’s a concept I’ve started calling “The Bridge Tax.” It’s the price you pay for using a cross-platform framework instead of going native. Every single interaction between your JavaScript code and the native platform—every animation, every gesture, every sensor reading—crosses a “bridge” between two runtimes. And every bridge crossing has a cost.

Sometimes the cost is negligible. A button tap crosses the bridge and you don’t notice. But when you’re trying to run a smooth 60fps animation while simultaneously reading accelerometer data and updating a real-time chart? The bridge becomes a bottleneck. Frames drop. The UI stutters. You start to feel the ghost of a Kotlin developer whispering in your ear: “You should have gone native.”

The new architecture (Fabric, TurboModules, JSI) is supposed to fix this by eliminating the bridge entirely and allowing JavaScript to call native functions synchronously. I have read the documentation four times. I understand approximately 40% of it. The other 60% feels like it was written by someone who has transcended the mortal plane of app development and is now communicating from a higher dimension where all builds succeed on the first try.

Acceptance

So here I am. I have gone from arguing about SIMD frequency scaling to arguing about whether a <ScrollView> should have contentInsetAdjustmentBehavior="automatic". I am writing Javascript again. I am managing state with Zustand. I am styling things with inline objects that look like CSS had a nervous breakdown and forgot how to use hyphens.

And you know what? It’s fine. Prior is working. The app is fast (enough). The animations are smooth (mostly). I can iterate rapidly, pushing fixes and features in hours instead of weeks. I have developed a sort of Stockholm Syndrome for the React Native ecosystem. It is chaotic, it is messy, and it is occasionally infuriating, but it lets me ship software to people’s pockets. And at the end of the day, that is what matters—not the purity of my toolchain, but whether the thing I built actually reaches a human being.

Just don’t ask me to look at the package-lock.json. Or the ios/Podfile.lock. Or, honestly, any file with “lock” in the name. I don’t want to know what’s in there.