Skip to content
All posts

BoilerplateReact NativeHeroUI NativeUniwind

How to choose a React Native boilerplate

Robin Faraj

A boilerplate gives you a project that already has a structure, so navigation, state management and authentication are answered before you start. You are taking on a set of decisions someone else made. Good ones save you time; bad ones cost more than starting from scratch would have.

Which is the whole problem with choosing one.

The libraries you are inheriting

Every boilerplate picks your dependencies for you, and you live with them for as long as the project lasts. Three of those choices matter more than the rest.

Component libraries decide what your screens are made of. HeroUI Native is styled with Tailwind CSS and ships accessible components, which is a short step if you already build for the web.

Styling decides how much of your CSS knowledge transfers. Uniwind brings Tailwind CSS v4 to React Native, so the syntax is the one you already use.

Navigation decides how your app is laid out on disk. Expo Router uses file-based routing, which works the way web routing does.

Beyond the picks themselves, look at whether the libraries are maintained. A boilerplate is a snapshot of a dependency tree on the day it was published, and an unmaintained dependency becomes your problem on the day React Native changes underneath it.

The structure you are inheriting

Organize around features rather than technical layers. A folder per feature that owns its screens, components, hooks, repository, API calls and tests beats a folder of every screen, a folder of every hook and a folder of every test, because a change to one part of the app then stays in one place.

Most projects claim this and few of them mean it. There is a test that settles it: can you delete a feature?

Each feature in NativeExpress lives in src/features/<name>/ and implements one type:

export type Feature = {
  id: string;
  tabName: string;
  tabLabelKey: string;
  starterLabelKey: string;
  icon: LucideIcon;
  Preview: ComponentType;
  startRoute: (isTablet: boolean) => Href;
  getRecents: (limit: number) => Promise<FeatureRecent[]>;
};

Features import shared infrastructure and never one another, and yarn check:features fails the build if one tries. The home screen calls getRecents and merges what comes back without knowing which tables it came from. On the server, each feature keeps its own edge function directory and its own SQL block in the migration, fenced between -- feature:chat:start and -- feature:chat:end.

The payoff is that deleting one is a command rather than an afternoon:

yarn remove:feature create

That pulls the registration, the directory, the route files, the edge function, the SQL block and the Maestro flow. There is a --dry-run for reading the damage first, and yarn check:feature-variants builds and tests every variant, Chat only, Create only, Scan only and the shell with no AI features at all, to prove none of them break.

You do not need our exact commands. You need to check that whichever boilerplate you pick can do the equivalent. If it cannot remove one of its own example features cleanly, its boundaries are decorative, and everything it ships that you do not want is now yours to unpick by hand.

Two smaller things worth checking while you are in there. Configuration should be per environment, so development, staging and production behave predictably. And the dependency list should contain only what the project uses, because every library you keep is one more thing to update for security and compatibility.

Authentication is the part to check hardest

Authentication is where a weak boilerplate costs you most, because it is the part you are least likely to rewrite and the part that leaks user data when it is wrong.

Token-based authentication with OAuth2 is the usual approach, and Supabase and Firebase both implement it, including generating and refreshing tokens so sessions survive without asking people to sign in again.

What to check in the implementation you are inheriting: tokens go into secure device storage rather than ordinary storage; every API call uses HTTPS; the server validates the token rather than trusting whatever the client sends; and failures tell the user what went wrong without describing the internals.

That last one sounds cosmetic. It is the difference between an error screen and a description of your auth flow handed to whoever is looking for one.

Judging one before you commit

Read the source of the parts you would not write yourself, which for most people means auth, payments and anything touching native code. If you cannot follow it on a first pass, you will not be able to fix it under pressure.

We build NativeExpress on the practices above. It ships with Supabase for the backend and RevenueCat for in-app purchases, both wired up rather than listed as options. Whether you use ours or someone else's, read that code before you build a project on top of it.