← Volver al blog

February 14, 2026·10 min read

That growing helper file—and how to tame it

Almost every Expo or React Native app I’ve touched ends up with a `helper.ts` (or `utils.ts`) that began as a few nice functions and slowly became the scariest import in the codebase. This is how I treat that module more like a thin service layer: obvious names, side effects fenced in, and seams clear enough that swapping storage or navigation later doesn’t mean rewriting half your screens. Expect concrete patterns for storage, navigation, and third-party glue—plus when to split the file before it owns your sprint.

What belongs in “helpers” (and what doesn’t)

Helpers, in my head, are the glue: save the session, fire a toast, open Maps, format money, wrap a permission prompt—stuff that shows up all over the app. A one-off formatter that a single screen uses? That can live next to that screen instead of swelling the shared file.

If someone new asks “where do we save tokens?” and the answer is “uh… somewhere in helper,” that’s a smell. Good helpers answer that question in one glance.

Storage: a few obvious functions beat ten clever ones

I expose small, boring names: save token, read token, clear session—whatever matches how your team talks. Screens go through those wrappers instead of importing SecureStore or AsyncStorage everywhere, so the day you change providers isn’t a repo-wide rewrite.

Jot down what’s encrypted, what happens when a read fails, and that these are async. Nobody needs abstraction gymnastics; they need the same pattern every time.

Navigation glue that won’t snap when you change routers

It’s tempting to thread router objects through everything. I prefer a handful of functions that say what you mean: go home, open auth, land on profile—implemented with whatever router you’re on today.

When Expo Router or your stack layout changes, you touch one file. I also try to keep router types imported in as few places as possible so the upgrade path stays narrow.

Maps, deep links, permissions—the messy middle

Static map URLs, `tel:`, camera permissions—this stuff mixes config (keys, schemes) with imperative APIs, which is why it lands in helpers. Centralize reading env vars and log something useful when a key is missing, without printing secrets.

If you return an empty string when misconfigured, one clear log line beats silent failure in production.

Toasts and notifications: same voice everywhere

Wrapping toasts and local notifications means users get consistent timing, copy, and accessibility labels—and design can tweak behavior without hunting through every screen.

Worth saying out loud: most of this only belongs on the client bundle, and some of it only makes sense on a real device.

When the file hits a few hundred lines

Split by topic—`storage.ts`, `navigation.ts`, `maps.ts`—and re-export from something like `helper/index.ts` so imports stay familiar. Before adding another export, I ask: is this used in more than one feature, and does it touch IO or a third-party SDK? If both answers are no, it probably shouldn’t live here.

The app utilities section on this site is basically a big, real-world example you can steal from and then delete half of because you don’t need it yet.

Signals that it is time to split the file

You know you’re past due when imports start chasing circular dependencies, two teams schedule around the same file, or onboarding still says “ask Sarah” for how tokens are saved. That’s the moment to carve modules by topic and keep a tiny barrel export so call sites do not thrash.

Drop a short README in the helpers folder: one line per module saying what it owns and what it must never import. It costs five minutes and saves weeks of archaeology.

Why the helper file exists in the first place

Mobile apps glue together asynchronous storage, navigation, maps, permissions, and toasts. Each of those APIs is small on its own, but screens do not want to import five modules and repeat error handling every time. A helper module starts as a kindness: `saveSession`, `openSupport`, `formatCurrency`. The trouble is kindness without boundaries. Pretty soon the helper imports your analytics client, your API base URL, and a stray component because someone needed a quick modal. At that point it is not a helper; it is an application kernel nobody planned. The goal of restructuring is not purity—it is predictability. You should be able to answer where side effects live, what is safe to call on startup, and what requires user interaction. Splitting helpers by topic makes those answers obvious: storage helpers never import UI, navigation helpers do not reach into SecureStore, and formatting stays pure. If you need cross-cutting orchestration—log in, then navigate, then track—write a tiny function that calls the smaller modules in order rather than merging concerns into one mega-export.

Designing storage wrappers people will actually use

Pick names that match how your team talks in standup: `clearSession`, `persistAuthTokens`, `readOnboardingState`. Avoid cute abbreviations that only one person understands. Document which calls hit encrypted storage versus AsyncStorage, and what happens when disk is full or the OS denies access. Return typed results instead of throwing raw exceptions into screens—map to a union like success, recoverable error, fatal error so UI can branch consistently. If you migrate keys, do it in one place with a version number stored alongside data. Log migration failures with enough context to debug (build number, old key presence) but never log secrets. For large JSON blobs, consider whether you are accidentally using key-value storage as a database; if queries get complex, move to SQLite or a real cache layer. Rate-limit writes if you are persisting on every keystroke. Tests at the storage boundary— even a few jest tests with mocks—catch regressions when someone “simplifies” async handling. Remember that background tasks and killed processes mean storage reads must always assume partial writes; atomic updates and checksums are not paranoia for payment-grade data.

Navigation helpers without tight coupling to routers

The router API changes between React Navigation major versions and when adopting Expo Router. If fifty files import `navigation.navigate` directly, upgrades hurt. Wrappers like `goToOrderDetails(orderId)` localize those calls. Keep the wrapper thin: map to route names and params, and avoid stashing navigation objects in global singletons unless you have a disciplined lifecycle story. Type the params at the wrapper boundary so screens do not re-parse strings. For auth flows, centralize the decision of which stack to show so you do not duplicate “if token then tab else auth” in three places. Deep links should also flow through the same helpers when possible so marketing URLs and in-app navigation share validation rules. When you cannot avoid passing navigation props—some libraries require it—isolate that to the screen layer and keep helpers ignorant. Document modal presentation rules in one place: which routes slide, which fade, and how Android back should behave. Inconsistent transitions confuse users more than slightly slower code.

Third-party glue: maps, links, permissions

These APIs fail in user-specific ways: missing keys, revoked permissions, regions where services behave differently. Centralizing them lets you log meaningful errors and show human copy. For maps, separate “build static preview URL” from “open native maps app” from “embed interactive map”—each has different failure modes and performance costs. For URLs and deep links, validate schemes before passing them to `Linking.openURL`. For permissions, wrap OS prompts with rationale screens when your product policy allows it, and handle “denied forever” by linking to settings with neutral wording. Never assume simulators match devices; exercise camera and location on hardware before release. When a vendor SDK wants initialization at startup, decide explicitly if that belongs in `App.tsx` or lazily on first use—lazy often improves cold start. Wrap vendor SDKs so swapping vendors does not require editing every screen. That wrapper is a good place to enforce consent gating for analytics and crash tools.

Splitting the file and keeping imports stable

When you split `helper.ts`, prefer topic files plus a barrel `index.ts` that re-exports the same public surface you had before. That minimizes churn in call sites. After the split, delete unused exports aggressively; dead code in helpers sends mixed signals about the “right” way to do things. Enforce import boundaries with lint rules if you can—eslint-plugin-import or custom rules—to prevent `storage` from importing `navigation` except through orchestration modules. Add a short README in the folder describing each module’s contract and listing forbidden imports. Onboardings should point new hires there instead of tribal knowledge. If you maintain a UI kit site like this project, link to real examples rather than copying huge blocks into docs; living code wins. Finally, schedule periodic reviews after major OS upgrades: permission APIs and background execution rules change, and helpers are where those changes concentrate.

Review checklist for helper changes

Before merging helper refactors, ask: Does every new export have a single clear purpose? Are side effects documented? Did you add tests or logging at the boundary? Will Android back and iOS gesture back still behave? Does accessibility copy for alerts and toasts still make sense? Could a junior dev import the wrong helper by accident—if yes, rename for clarity. Helpers are force multipliers; they are also incident multipliers when wrong. Treat edits with the same seriousness as auth code, because helpers often touch auth. After release, watch crash and ANR rates for a week; subtle async mistakes show up under load, not in dev. If metrics look flat or better, document what you learned for the next split. Continuous improvement beats one perfect architecture that never ships.

Mocking helpers in tests without losing confidence

Unit tests love pure functions; helpers are rarely pure. Establish seams: inject storage or navigation adapters in test doubles, or mock at the module boundary with clear setup/teardown. Avoid mocking everything—sometimes an integration-style test that runs against an in-memory fake storage gives more confidence than brittle expect chains. Document which helpers are safe to call in Node tests versus which require a native environment; tag tests accordingly so CI does not become a sea of skips. When a bug slips through, add the smallest test that would have caught it, even if it lives in an “uncomfortable” layer. Over time, your helper test suite becomes living documentation of side effects and ordering assumptions. Pair that with occasional manual testing on low-storage devices and airplane mode—conditions unit tests rarely simulate. The goal is not 100% coverage of lines; it is coverage of contracts that users depend on.

Shipping and reliability habits (1)

Push notifications walk a line between helpful and intrusive. Prime users with context, respect notification channels on Android, and measure opt-outs after campaigns—spikes mean copy or frequency problems. Payload design affects background behavior; test killed and locked-device states. Tokens belong server-side with rotation strategies; never treat the client as authoritative for subscription state.

Images and maps look simple in mocks and expensive in production. Decode sizes should match display sizes; static map thumbnails are not interchangeable with interactive MapView gestures. Quotas, API keys, and offline behavior need explicit fallbacks—addresses as text, retry buttons, and calm error copy. Monitor vendor dashboards for spikes that indicate bugs or abuse.

Platform differences worth rehearsing (2)

Type-safe navigation pays off when routes multiply. Keep param lists near navigators, validate external URLs, and avoid serializing non-JSON-safe values through params. Renaming routes is a cross-cutting change—update analytics, push payloads, and E2E selectors in the same release train.

FlatList performance is configuration as much as code. Stable keys, reasonable `windowSize`, and memoized rows beat switching to a different list primitive blindly. Nested virtualized lists are a last resort—redesign first. Profile with production-like data volumes; dev placeholders lie.

Security, privacy, and data handling (3)

Reanimated and gesture libraries earn their place when profiling proves UI-thread work and your team can maintain native upgrades. Worklets have constraints—read errors carefully. Respect reduced motion and test Android timing differences—identical JS does not guarantee identical feel.

Analytics schema governance prevents warehouse disasters: version events, avoid high-cardinality strings, and align names across iOS, Android, and web. Consent gating must stop network calls, not just UI. Separate dev and prod projects to avoid polluting dashboards.

Patrocinado

Promoción breve