16 min read

How I became a React Native developer — the apps I shipped from 2020 to 2023

I saw where cross-platform mobile was going and moved towards it. Between 2020 and 2023 I shipped and refactored more apps than I can count — a dating app with no swiping, a golf app, a freight app, a P2P training simulator for first responders. Here is what that run taught me.

CareerMobileAndroidiOS

I was writing React when it became obvious where cross-platform mobile was heading. One codebase, two platforms, and a language I already worked in every day — that was a very good opportunity, and I moved towards it deliberately.

The run I care about is 2020 to 2023. In those three years I shipped and refactored more apps than I can now count, and almost none of them were the app you imagine when someone says "React Native app." They were odd. Each one had a single hard part that had nothing to do with mobile development, and everything to do with the domain it came from.

Here are the ones I remember.

A dating app with no swiping

US-only, and built on the idea that swiping is a poor way to meet someone. There was no card deck. The feed was the product, and users could engage with audio, video and gaming elements directly inside it — the point being to recreate the feeling of bonding over a real-life event.

So the feed was the hard part. A feed of static cards is a solved problem. A feed where any item might be playing audio, playing video, or running a game has to track what is active, what is paused, and what gets torn down as you scroll — without draining the battery or leaving four audio streams running at once. Matching was never the interesting half.

An app for golfers

Find other players by searching, join or create a group, start an RSVP event, and coordinate the round with everyone in it. Underneath all of that sat availability — each player publishing when they could actually play, so a group could find a window that worked instead of trading messages until someone gave up.

Golf turns out to be a scheduling problem wearing a sport costume. Who is playing, on what day, in which group, against whom, and what happens to the standings after. RSVP is the easy half; the hard half is that an event is only real once enough of the right people are free at the same time.

There was an in-app purchase to upgrade. The paid tier added finer availability slots and let you link a Google or Outlook calendar, so the app could read your existing commitments and treat them as unavailable. That is the feature that justified the subscription — not more golf, but not having to choose between a round and something at work you had already agreed to.

Two calendar providers, two OAuth flows, two sync models, and a rule that a busy block from either one wins. Users see one merged view of when they are free. Everything that makes that view simple lives behind it.

It was also the cleanest interface I built in that period. Worth saying, because a coordination app earns its place by feeling lighter than the group chat it replaces. The moment it feels like admin, people go back to texting.

A route app, Porter for the US

If you have used Porter in India you know the shape: someone has a thing that needs to get from A to B, someone else has a vehicle. One app, two completely different user types.

The flow ran on bidding rather than a fixed rate. A sender creates a job, drivers place bids on it, the sender picks one, and the route starts. From there the driver opens the job and drives to the pickup. Nobody is assigned anything — both sides opt in, which is the part that makes the marketplace work and the software harder.

Two user types in one codebase is the thing I would call out. A driver and a sender share an app, a login and a job object, and agree on almost nothing else. Different home screens, different permissions, different notifications, and a different meaning for the same word — "my jobs" is work you are doing on one side and deliveries you are waiting on from the other. Every screen has to ask who is looking at it.

Three systems sat around that:

  • Driver eKYC, in-app. You cannot let an unverified stranger collect someone's belongings, so identity verification had to happen before a driver could bid at all — with document capture that works on a mid-range phone in bad light.
  • Ratings on both sides. Drivers rate senders and senders rate drivers. Two-sided trust is what makes anyone accept a bid from a person they have never met.
  • A wallet. Money holds in the app across a job rather than moving hand to hand at the door. That means a balance that stays correct while a job is in flight, and stays correct if the job goes wrong.

The wallet was the part with the least room for error. A delivery that fails is a bad day; a balance that is wrong is a support ticket that never fully closes.

A food ordering app

There are a thousand food ordering apps. This one earned its existence on customisation — the number of ways a single item could be modified before it reached the kitchen — and on being visibly better looking than what it competed with.

Deep customisation and a clean interface pull against each other. Every extra option is another row on the screen. Most of the work was hiding complexity until the moment someone wanted it.

The home screen listed restaurants by location and rating, which is table stakes until you try to make the ranking feel right rather than merely correct. Payments went through Stripe. Orders could be edited, and any past order could be reordered — with its customisation intact, which is the whole point. Nobody wants to rebuild a sandwich they have already specified once. The same idea ran through favourites: you could favourite a restaurant, and you could favourite a dish with your modifications already applied, so ordering it again was one tap rather than eight.

The part I had not built before was dine-in. Switch to dine-in mode and the app used Bluetooth beacons to reserve your table — the phone picks up the beacon in the restaurant, works out where you are, and holds the table without anyone typing a table number or scanning anything.

Beacons are a good idea that fights you in practice. Bluetooth ranging is approximate, so proximity is a probability rather than a fact, and two tables sitting near each other look similar to a phone. Both platforms treat background scanning as a battery and privacy concern and restrict it accordingly, and the permission prompts differ on iOS and Android. When it works, it feels like the app simply knew — but most of the effort goes into the cases where the signal is ambiguous and the app still has to make one confident decision.

An edtech app with locked levels

Learning content split into levels, each one gated behind a quiz. Pass, unlock, continue.

The gate is what makes it work and what makes it hard. Progress has to be right, it has to survive a reinstall, and it has to be tamper-resistant enough that unlocking chapter nine is not a matter of editing local state.

A wallet and NFT app

Log in with MetaMask or Trust Wallet, and the app fetches the NFTs that wallet holds and puts them on the home screen. Pick one, choose a background colour, and the app generates a dynamic URL that casts it — a link you can share with anyone.

This was the one that behaved least like a normal app. Wallet login is someone else's flow that you hand control to and hope comes back — the user leaves for MetaMask or Trust Wallet, approves something in an app you do not control, and returns. Fetching tokens is a slow, unreliable network call dressed up as a gallery. And the cast URL meant a mobile app had to produce something that worked outside itself, for a person who did not have the app installed.

The part I did not expect to be the hard part was the colour picker.

The client wanted a specific colour wheel. Not a palette of swatches, not a row of preset backgrounds — a real wheel, behaving the way they had in mind. Nothing available did it, and doing it in JavaScript meant fighting the bridge for something that has to track a finger with no perceptible delay.

So I wrote it as a native library and linked it into the React Native codebase. That is the project where I stopped treating native as somebody else's layer. Once you have shipped one native module, "React Native cannot do that" turns into "nothing in the ecosystem does that yet" — a much smaller problem.

The app ran on the Expo bare workflow, which is what made this practical. Managed Expo gives you a great deal and takes custom native code off the table. Bare keeps the parts of Expo worth keeping and hands the native projects back to you. For an app that needed wallet SDKs and a native module I wrote myself, it was the only workflow that fit.

A P2P training simulator for first responders

The one I am most attached to. A real multi-user app that taught first responders in the US how to take control of an emergency scene, with WebSockets underneath so every action appeared live for everyone else in the room.

A trainer sets up a session: crop a small section of a map, add participants by email address, and assign each one a role — firefighter, police, and so on. Those people log in, see a join option, and enter the room.

The role decides what you get. Each one comes with its own set of props — vehicles, signage, the equipment that role would actually bring — and you place them on the map, then move, resize, rotate or delete them. You control your own props and nobody else's. The trainer controls everything. Then a scenario runs, and the trainer teaches people how to respond to it by watching what they do and correcting it as it happens.

So it is a real-time collaborative editor whose objects are owned by role. That sentence is short and the implications are not.

Every action is a message and every message needs an owner. Placing a vehicle, dragging it four inches, rotating it, deleting it — each one goes to the server, gets checked against who is allowed to touch that object, and comes back out to everyone in the room. The permission check cannot live in the interface. Hiding the drag handle on someone else's prop is a courtesy; the server refusing the message is the actual rule. And the trainer's override has to run through the same path rather than around it, or you end up with two code paths for one operation and only one of them tested.

Then the ordinary problems of shared state show up. Two participants drag props at the same moment and both sets of updates have to land somewhere sensible. Someone's phone drops mid-scenario and has to rejoin into a room that moved on without them — which means the room's state has to be reconstructible on demand, not just streamed. Continuous gestures make it worse: a drag is not one event, it is a flood of them, and sending every frame to every participant is how you melt a network. Sending too few makes other people's props teleport.

None of that is mobile work. It is distributed systems work that happens to be rendered on a phone. This is the project that taught me the difference, and it is still the one I would point at if someone asked what React Native is capable of.

A Practo-style app for France

Find a doctor by rating and cost, book a slot, pay, and have the consultation — online or in person. Doctors get their own calendar to set when they are available. Online consultations happen over video in the app, and the doctor writes notes once the call ends.

Two sides again, but unlike the route app these two want opposite things from the same screen. A patient wants the soonest appointment that fits their day. A doctor wants control over a working week that is already mostly spoken for. Availability is where those meet.

I built the calendar from scratch. Something Calendly-shaped, showing bookable slots — because slot rendering is deceptively specific. Availability is not a list of times. It is a set of rules intersected with existing bookings, consultation length, and whatever the doctor has already blocked out, then projected into the timezone of whoever is looking at it. Change the consultation length and every slot boundary moves. Nothing off the shelf fit, so I wrote it.

The other half was the consultation itself. Video ran peer-to-peer through Twilio, which is the right way round for this — a doctor and a patient talking is exactly two participants, and keeping the media between them rather than routing it through a server is better for both latency and for not holding clinical video anywhere you do not have to.

Notes are what turn a video call into a medical record. The consultation is not finished when the call ends; it is finished when the doctor has written up what happened. Getting that to feel like part of the same flow, rather than a separate chore afterwards, mattered more than any of the video work.

Building this for France also meant the boring parts were not optional. Health data, in the EU, in an app that handles both payments and consultations. That constrains what you keep, where it goes, and how long it lives.

A farm management app

Track the whole rearing process for farm animals, by hand. Health, head count, meat, and everything around it — what was purchased, what resources were consumed, what is left. An inventory system with livestock in the middle of it.

The work here is not one hard feature. It is breadth. An animal has a history: it arrives, it is fed, it is treated, it is weighed, it produces or it is processed, and every one of those is an entry someone has to make. Feed and medicine come in as purchases and leave as consumption, so the inventory has to reconcile against what the animals actually used. Get any of it slightly wrong and the numbers stop agreeing with the barn, which is the only thing the app is for.

"Manually" is the word that shapes the whole product. There are no sensors and no automatic readings — a person enters everything. So the entry screens are the app. Anything that takes three taps instead of one gets skipped, and a skipped entry is worse than no system at all, because now the total is wrong and looks right.

This was also the one built specifically for phones and tablets. A tablet is not a large phone. The extra width is only useful if you spend it — a list and its detail side by side, a table that shows the columns a phone has to hide, a form that does not become a single stretched column of inputs. Two layouts, one codebase, and every screen has to answer what it does with the space. On a farm the tablet is the one that sits where the records get kept and the phone is the one that goes out to the animals, so they are not the same tool doing the same job.

A trampoline booking app

For an amusement park. Pick a session, book a slot, turn up and jump.

Booking against physical capacity is stricter than it looks. A trampoline park has a real number of people it can hold in a real time window, and the app is the only thing standing between that number and a queue of parents at the door. Overselling is not a soft failure.


What that run actually teaches you

There are more. I lost count somewhere in the middle, and I will add them here as I remember them. But the pattern is already clear from these.

The mobile part is rarely the hard part. Not one of these apps was difficult because it was React Native. The dating app was hard because of mixed media in a feed. The training simulator was hard because of concurrency. The France app was hard because of calendar rules. The route app was hard because a wallet has to stay correct. React Native was the medium, not the problem — which is exactly what you want from a tool.

Niche apps make you learn domains. Golf handicaps. Freight pricing. Livestock records. Emergency response procedure. You cannot build these from the outside; you have to learn enough of someone else's job to know which of their edge cases are real. That is the part nobody puts in a job description, and it is most of what I did.

Refactoring taught me more than greenfield. A good share of that run was walking into an existing codebase and making it survivable. New projects teach you what you already believe. Old ones teach you what actually holds up after eighteen months and four developers.

Everything repeats. The same button. The same sheet. The same input. The same fade on mount, written from nothing, on project after project. By the third year running it stopped feeling like work and started feeling like a missing library.

I moved towards cross-platform mobile because the opportunity was obvious. What I did not expect was how much of the job would turn out to be learning other people's work — golf, freight, emergency response, farming — and how little of it would be about the framework.