React Native Flow

← Back to the blog

February 10, 2026·10 min read

Folder structure that doesn’t fall apart as you grow

Tiny apps forgive messy folders. Bigger ones don’t—you end up with three “Profile” screens, a `utils` folder nobody dares open, and new teammates asking where the important stuff actually lives. Here’s how I think about layout for Expo Router and plain React Navigation: not a rigid template, just lines in the sand that still make sense after your file count doubles. You’ll leave with a simple mental model for where routes, features, and shared infrastructure live, plus a safer way to refactor without freezing the team.

When sorting everything into components / screens / hooks stops working

At first, root-level `components`, `screens`, `hooks`, and `utils` feels fine. Twenty files in, it still feels fine. Two hundred in, `utils` has become the junk drawer and every import looks identical in a PR.

What usually helps is drawing clearer lines: navigation worries about routes, a feature owns its own UI and local state, and the boring stuff everyone shares—HTTP client, storage keys, analytics—lives in a small, deliberately named layer. You don’t need the perfect layout on day one; you need the next cleanup to be smaller, not bigger.

Let your routes be the backbone

File-based routes in Expo Router or a stack from React Navigation—either way, that tree is the spine. I like screens thin: pull data and behavior into hooks and small components instead of one 400-line file that does everything.

Keep the fiddly route stuff (params, headers, deep links) near the navigator. If one screen gets heavy, a colocated hook like `useCheckoutScreen` next to that folder makes the relationship obvious to the next person who opens the repo.

Keep feature code next to the feature—until it’s really shared

If only one product area uses a list or form, it probably shouldn’t live in a global components folder yet. Shared UI belongs in something you treat like an internal kit; everything else can sit beside the feature until a second, unrelated screen genuinely needs it.

When you do promote something to shared, do it on purpose: stable props, a quick comment on how you expect it to be used, and resist the urge to stuff one-off props into a “generic” component that only one caller touches.

One boring place for APIs and storage

Fetch and persistence shouldn’t be an Easter egg hunt. I aim for one place (or a handful) that knows base URLs, auth headers, refresh, and how to turn errors into something the UI can show. Screens call `getProfile()` or `saveSettings()`, not raw `fetch` in fifteen files.

Same for AsyncStorage and secure storage: key names and any migration notes live in one module so you’re not grepping the whole app six months later wondering which string you picked.

Expo Router vs React Navigation—same idea, different wiring

Expo Router nudges you toward colocation because routes are files; I still split non-route code into feature-shaped folders when it helps. With React Navigation, I group stacks and tabs by product area instead of one flat `screens` dump.

Either way, boring naming wins: `SomethingScreen`, `useSomethingScreen`, and a clear split between “this paints pixels” and “this loads data.”

Before you move every folder in one weekend

Write down what actually hurts—onboarding time, merge pain, circular imports. Then refactor one vertical slice (one feature or one navigator) and ship it. Shake out deep links and lazy imports on both iOS and Android as you go.

Big-bang rewrites without tests are how structure projects turn into regret. If you want a downloadable tree to compare against, the project structure doc on this site is there for exactly that.

If you only remember three things

Treat routes and navigators as the backbone: screens stay thin; heavy logic lives in colocated hooks and components with boring, searchable names. Keep feature code beside the feature until a second product area genuinely needs it—then promote shared UI like an internal kit with stable props. Refactor in vertical slices and test on real devices; a structure that only “works in CI” still fails when a user opens a cold deep link.

What changes when the team doubles

When you are two people, you can hold the whole graph of imports in your head. When you are eight, the same flat `screens` folder becomes a negotiation: two features touch checkout, someone adds a fourth variant of “Settings,” and PRs start conflicting because unrelated teams edit the same barrel file. The structure you need is not the one that looks prettiest in a diagram; it is the one that reduces the blast radius of a change. That usually means routes stay obvious, feature code stays near the product surface it serves, and shared infrastructure is boring enough that nobody feels clever when they edit it. I have watched teams try to fix this with a “big rename” that moves hundreds of files in one branch. It feels productive for a weekend, and then reality arrives: cherry-picks, long-lived release branches, and hotfixes that still reference old paths in crash logs. The alternative is incremental: pick one navigator or one product vertical, move it, ship it, and keep CI green the whole way. Your folder layout should make those incremental moves possible. If every import path has to change when you touch one feature, your spine is too rigid. If nothing is shared and every feature reimplements HTTP and storage, your spine is too loose. The balance is a small set of well-named shared modules—network, auth session, analytics facade—and everything else living where a product owner could describe ownership without reading `package.json`.

Expo Router files versus feature folders in practice

Expo Router rewards you with a URL-shaped tree: `(tabs)`, dynamic `[id]`, groups in parentheses. That tree is excellent for answering “what routes exist?” It is less excellent on its own for answering “where does checkout business logic live?” My habit is to keep route files thin: they compose hooks and presentational pieces, and they declare headers, suspense boundaries, and deep-link param wiring. Beside the route, a folder can hold `components`, `hooks`, and `model` for that slice of the product. React Navigation users get a parallel shape: the stack definition file lists screens, but the heavy lifting sits in feature folders keyed by domain. The failure mode to avoid is copying web SPA habits blindly—dumping “containers” and “presentational” everywhere without tying them to a navigable unit of work. Mobile users do not navigate your repo; they navigate stacks and tabs. Aligning code boundaries to navigation boundaries pays off when you need to lazy-load a heavy screen, gate it behind a feature flag, or rip it out after an experiment ends. Another failure mode is over-colocating: five one-off components beside a screen that will never be reused. It is fine to lift them later. Premature sharing creates abstract components with props that only one caller understands. Let duplication live until the second caller appears; then extract with a name that matches how designers talk about the UI.

Shared UI versus app-specific UI

Internal UI kits are powerful when they behave like a product: versioned-ish APIs, visual regression tests, and a short usage note in the repo. They fail when they become a junk drawer of props that exist because one screen needed a corner case. I treat shared components like a published library even if consumers are only internal: stable props, predictable accessibility defaults, and no sneaky global reads. For everything else—experiment screens, campaign landing routes, one-off charts—keep them in the feature until reuse is real. The same rule applies to hooks. A `useProfile` hook that hits your API belongs near profile until two unrelated areas need the same contract; then promote it and document cache keys and error semantics. Naming matters more than folder depth. `features/checkout/hooks/useCheckoutPayment.ts` tells a story. `hooks/useThing.ts` does not. When you promote something to shared, add a one-line comment at the export: who should use it, and what it must never import (often navigation objects or feature-specific stores). Those guardrails prevent shared layers from turning into a second app that imports half the tree. Dark mode, density, and localization are good forcing functions: if a shared component cannot render in those modes without special cases in every caller, its API is not ready.

APIs, clients, and where fetch should live

Screens should not own URL strings, header injection, or token refresh choreography. A thin API client module—sometimes one per backend, sometimes one per bounded context—centralizes those concerns. Screens call functions that return typed data or typed errors, and they map those errors to UI states. This is not just cleanliness; it is how you survive OAuth rotation, base URL changes between staging and prod, and the day you add request signing. Pair that with a single place that defines storage keys and migration notes for AsyncStorage or secure storage. When those keys sprawl, you get subtle bugs: one code path writes `auth_token`, another reads `authToken`, and logout only clears one of them. Document the happy path and the recovery path: what happens on read failure, on corrupted JSON, on first launch after upgrade. Testing this layer with integration-style tests—even lightweight ones—pays off more than snapshotting giant screens. Finally, keep analytics and logging out of leaf components when you can. A facade that knows how to attach route names and user-consent flags prevents PII from leaking through copy-paste `track()` calls.

Refactors that survive release trains

If you ship weekly or faster, structure work has to be mergeable in slices. That means feature flags for risky moves, backwards-compatible exports during transitions, and clear ownership of who fixes conflicts in shared files. Before a big move, write down the risky edges: deep links, notification taps, background tasks that assume a screen name, and E2E tests that hardcode paths. Update those in the same vertical slice as the folder change. Communicate the new map in one short doc: three bullet points on where routes, features, and shared code live, plus a diagram link. Onboarding time should improve after the change—measure it by asking a new hire to add a screen and timing how long until their PR merges. If it gets worse, your structure is still fighting your workflow. Lastly, do not chase perfection. Repositories are living systems; optimize for the next six months of work, leave breadcrumbs for the next person, and prefer boring clarity over clever abstraction.

Appendix: questions to ask before you approve a structure PR

Use this as a lightweight checklist in code review. Can a new contributor find the home screen implementation in under two minutes? If a feature is removed, is it obvious which folders to delete? Do shared modules avoid importing feature routes? Are environment-specific values absent from feature folders? Does CI still run typecheck and tests without special casing new aliases? Could you explain the deep-link mapping from marketing URLs to files without opening ten different README files? If any answer is shaky, tighten naming or add a short `ARCHITECTURE.md` at the repo root—not a novel, just a map. Structure PRs are expensive; spending thirty minutes on documentation while memory is fresh saves days later. Finally, revisit the layout after major product pivots: the best structure for a single-user tool is not always the best for teams, subscriptions, or offline-first modes. Schedule a quarterly glance, even if the outcome is “no change,” so the discussion stays alive.

Coordinating with design, QA, and release management

Folder structure is an engineering artifact, but it affects everyone. Designers care where they should place Figma files relative to screens they can name consistently with your routes. QA cares that test plans map to navigable areas without ambiguous overlaps. Release managers care that feature flags and changelog entries line up with folders they can trace to owners. Spend a one-hour workshop aligning vocabulary: if design calls a surface “Wallet” and your route is `finance/cards`, reconcile that early. QA benefits from a printed map of stacks and tabs—mundane, effective. Release managers benefit when crash grouping keys include route or feature names supplied by your logging facade. None of this requires perfect alignment on day one; it requires explicit owners and a willingness to rename when product language shifts. When you rename, do it with the same slice-based discipline as code moves: update docs, tests, and analytics event names in the same train. The worst outcome is three names for the same surface in different tools—support tickets become impossible to triage. Treat naming drift as debt with interest.

Sponsored

Quick promo