Inside the State Layer: How RapidNative Generates React Native State Management
By Rishav
11th Jun 2026
Last updated: 11th Jun 2026
When a human engineer starts a new React Native project, the first hour usually disappears into a familiar question: what should we use for state management? Redux Toolkit, Zustand, Jotai, Recoil, MobX, plain Context, or the latest signals library that hit Hacker News this week. The choice ripples through the rest of the codebase — every screen, every fetch, every form.
When an AI builds a React Native app, that decision has to be made before the user types their first prompt. The model can't stop mid-generation to ask, "by the way, do you prefer Zustand or Redux?" Every component it produces has to fit a state architecture that's already been chosen on the user's behalf — one that has to work for a to-do list, a marketplace, a fitness tracker, and a B2B internal tool, with no per-project tuning.
This post is a tour of the state architecture RapidNative generates inside every app — what we pick, what we deliberately ban, and why an AI code generator has different state-management constraints than a human developer.
State decisions in React Native ripple through every screen — for an AI generator, those decisions must be made up front. Photo by UX Indonesia on Unsplash
The constraint nobody talks about: AI can't refactor between screens
Before we get to the layers, it helps to understand the constraint that shapes everything. A human developer is allowed to start with useState everywhere and gradually lift state up as the app grows. They can refactor a prop-drilling mess into a Zustand store on a Tuesday afternoon, and nobody minds. The codebase evolves.
An LLM generating code doesn't have that luxury. Each screen is generated in a relatively isolated context window. If the model picks useState on screen one and Redux on screen four, you don't get a refactor — you get a Frankenstein app that nobody can maintain. Consistency is more valuable than sophistication. That single observation drives every state decision in the platform.
The second constraint is that most users typing prompts into an AI mobile app builder are not React Native experts. If the AI generates idiomatic Redux Toolkit code with slices, selectors, and createAsyncThunk, the user can't reason about it, debug it, or extend it later. They'll re-prompt instead of editing — which is fine for prototyping, but a problem for shippable apps.
Both constraints push toward the same conclusion: pick the simplest, most uniform state architecture that can still handle real production apps, and enforce it at the prompt level so the model never wanders.
The four-layer state model RapidNative generates
Every app the platform generates uses the same four-layer model. Most React Native posts present these layers as choices; we present them as roles. Each layer has exactly one tool, and the AI knows which layer a piece of state belongs to.
| Layer | Concern | Tool |
|---|---|---|
| Local UI state | Toggles, inputs, modal open/closed | useState |
| Server state | Data fetched from the database | TanStack Query v5 (React Query) |
| Database access | Reading and writing rows | Vibecode DB adapter (Supabase / mock) |
| Persistence | Survives app restarts | AsyncStorage (via React Query persister) |
What's missing from this table is just as important as what's in it. There is no entry for Redux. No entry for Zustand. No entry for Context. The system prompt that drives generation explicitly forbids those patterns in user code. We'll get to why in a moment.
Layer 1: Local UI state stays in the screen
The first rule is the simplest: anything that only one screen cares about — a search input value, whether a modal is open, the selected tab on a settings page — lives in that screen via useState. Plus useCallback, useMemo, and useRef for the obvious reasons.
That's it. No useReducer, even for moderately complex forms. No custom hooks that abstract several useState calls behind one signature, unless the abstraction is genuinely reused. The point isn't that useReducer is bad — it's that the AI doesn't get to decide unpredictably between useState and useReducer based on which examples were in its context. Pick one, use it everywhere, move on.
In practice, this means generated screens look like the React Native that beginners write in tutorials. That's a feature, not a bug. The user can read it. The next prompt the user types ("make the cart icon show a badge when there are items") can be applied without reasoning about a store.
Layer 2: Server state lives in React Query
The second rule is where things get more interesting. Any data that comes from the server — including data the user just wrote — goes through TanStack Query v5, with one critical restriction: object syntax only.
// What the AI generates:
const { data: posts, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: async () => {
const { data, error } = await client.from('posts').select('*');
if (error) throw error;
return data;
},
});
// What the AI is forbidden from generating:
const { data } = useQuery(['posts'], fetchPosts); // tuple syntax, banned
That tuple syntax is older React Query, removed in v5. We mention it because it's the kind of thing an LLM with a wide training set will produce by default — a chunk of its training data predates v5. The fix isn't model magic; it's a system prompt that explicitly bans the tuple form and shows the object form in every example.
The other rule is more subtle. Notice that the query function returns the data field from a { data, error } object, not an array. Every database call in a generated app follows this Supabase-style result envelope, even when the underlying adapter isn't Supabase. We picked that pattern for a reason — see Layer 3 — and we enforce it everywhere so the AI never gets confused about whether data is an array or an envelope.
Mutations get the same treatment. Every write goes through useMutation with onSuccess calling queryClient.invalidateQueries({ queryKey: ['posts'] }). Manual cache writes via setQueryData are discouraged unless the AI is implementing an optimistic update, because the model is bad at picking the right key shape when it tries to be clever.
Four layers, one tool per layer. The boring choice is the consistent one. Photo by ThisisEngineering on Unsplash
Layer 3: Database access through a swappable adapter
The third layer is where most AI app builders get stuck. You want generated apps to use a real backend, but you don't want to make the user sign up for Supabase before they've even seen the preview render. You also don't want the generated code to be tightly coupled to whichever backend you happened to choose this quarter.
The platform's answer is a single client interface, the Vibecode DB adapter, with three concrete adapters underneath: a MockAdapter backed by AsyncStorage (with seed data and auto-sign-in), a SupabaseAdapter, and a PocketBaseAdapter. The adapter is chosen at runtime via an environment variable — EXPO_PUBLIC_ADAPTER_TYPE — so the same generated code runs against an in-browser mock during preview and against a real backend after the user connects one.
Inside a screen, the AI doesn't import Supabase or PocketBase directly. It reaches for the client through a context hook:
import { useApp } from '@/contexts/AppContext';
function PostsScreen() {
const { client } = useApp();
// client.from('posts').select('*') — same shape regardless of adapter
}
Auth follows the same pattern through a useAuth() hook that wraps client.auth mutations and automatically invalidates the right queries on sign-in and sign-out. The AI never has to write the auth boilerplate; it just calls the hook.
The deliberate consequence of this design is that "switching backends" is not a generation-time decision. The AI generates one set of code, and the user picks the adapter when they're ready. That keeps the prompt surface smaller and the generated code more predictable. For a deeper look at how the rest of the code generation pipeline streams components in real time, that's a separate post worth reading.
Layer 4: Persistence via the React Query persister
Layer four is the one most React Native tutorials skip. A real app needs to survive backgrounding, force-quits, and airplane-mode launches without showing a spinner. The naive answer is "wire up AsyncStorage for each piece of state you care about." The maintainable answer is "let React Query do it."
The platform configures the query client with createAsyncStoragePersister, keyed under REACT_QUERY_OFFLINE_CACHE, throttled to one disk write per second to avoid hammering AsyncStorage on rapid updates, with a 24-hour cache lifetime. Crucially, auth queries are excluded from persistence — you don't want a stale session sitting in disk cache after the user signs out. That's enforced by a dehydrateOptions.shouldDehydrateQuery check that filters auth-related query keys.
The practical effect is that an AI-generated app, with zero extra code per screen, feels offline-first. Cold-start renders show last-known data immediately while React Query refetches in the background. The user types the prompt, watches the preview render their feed screen, and the feed screen is silently durable.
There's one more subtlety. In designer mode — when the user is editing the app inside the platform's preview — the persister is disabled and stale-time is zero. We always want the editor to show fresh data so that edits to seed data show up immediately. In standalone mode — when the app runs on a real device — the five-minute stale time and persister kick in. Same code, different runtime behavior, controlled by one environment flag.
The system prompt that enforces all of this
We've been saying "the AI is forbidden from..." throughout this post. That's not a metaphor — it's a concrete list of rules in the system prompt that drives code generation. A few highlights, paraphrased so we're not reprinting our prompt verbatim:
- No Redux, no Zustand, no third-party state libraries. Local state uses React's built-in hooks. Shared state goes through React Query or context hooks that wrap React Query.
- No
useReducerunless it's modeling a state machine with three or more transitions. For two-state toggles, use auseStateofboolean. - TanStack Query v5 object syntax only. Tuple syntax is removed; any model output containing it is rejected at validation.
- Database results are always
{ data, error }envelopes. The model is forbidden from destructuring the result as an array. - No dynamic-routing hooks. No
useSearchParams,useLocalSearchParams, orusePathname. Navigation isrouter.push('/path')or<Link href="/path" />only. - No Context Providers in generated screens. Context lives in the scaffold (one
AppProvider, oneAuthProvider), and screens consume hooks. The AI doesn't get to invent new providers.
This list is the unglamorous part of an AI code generator. Most of the visible quality wins don't come from a better model — they come from narrowing the model's choices until the only paths it can take are the ones that compose.
The system prompt narrows the model's choices to the patterns that compose well. Photo by Fotis Fotopoulos on Unsplash
The meta-layer: how the platform itself manages state
So far we've talked about state in generated apps. There's a second, more elaborate state system inside the editor itself — and here, the choice is reversed. The platform uses Redux Toolkit with five slices (app, editor, themeEditor, integration, recovery) plus RTK Query's baseApi.
Why the asymmetry? Because the platform is built by engineers who understand Redux, ships on the web, and needs to coordinate dozens of concerns: chat history with optimistic writes, multi-file edits, runtime errors hashed for deduplication, artboards with positions and dimensions, zoom level, selected layers, team context, credits, subscription tier. The trade-offs that make Redux a bad default for AI-generated apps (verbosity, ceremony, beginner-unfriendliness) are the same trade-offs that make it a good choice for a complex editor maintained by a small team that knows the patterns.
A few interesting pieces of the platform's own state layer:
Tab-isolated sessionStorage. The currently selected team is stored in both Redux and sessionStorage, with a per-tab tabId. A user can open the same project in two browser tabs and operate as two different teams in each — because the source of truth is the tab's session, not the user row in the database. The database's selected_team_id column is a fallback for initial load only. Modifying it during team-switching would break the tab-isolation invariant.
Socket.io sync that skips itself. When a file changes in one tab, the worker emits a files:updated event that all tabs receive. Each tab checks the source tabId and ignores events it produced itself, so the same tab doesn't process its own writes twice. Cross-tab writes immediately dispatch a saveFile() thunk to Redux — no backend round-trip required.
Shared iframe database tables. This one is the cleverest. The editor renders multiple preview iframes — one per screen, plus a device preview — and each iframe ordinarily would have its own database. In an AI-generated app, that would mean a POST from the cart screen wouldn't show up on the orders screen until the next page load. The fix is a single Map<string, Record<string, any>[]> of tables, shared across iframes. When a new iframe loads, the platform points its adapter's tables reference at the existing shared Map — so a mutation in one preview is immediately visible in all the others, with no polling.
The recovery orchestrator. When generated code throws a runtime error, a four-layer recovery cascade kicks in: silent fix-with-AI, iframe reload, full-page reload (sessionStorage persists across the reload), and finally a user-facing toast. The orchestrator is gated by a sessionActivated flag — it only auto-fixes errors after the user sends their first message, so the platform never tries to "fix" code the user is currently writing. Errors are deduplicated by an MD5 hash of the error text so the same crash in a render loop doesn't trigger 100 fix attempts.
Why this approach beats Redux for AI-generated apps
Let's pull the threads together. The state architecture for generated apps boils down to four rules:
- Local state goes in the screen via
useState. - Server state goes through TanStack Query with strict syntax rules.
- Database access happens through a runtime-swappable adapter.
- Persistence is handled at the query-client level, not per-screen.
That's it. There's no twelfth chapter on how to organize selectors. There's no "if the app grows, we'll add a store" escape hatch. There doesn't need to be one, because the architecture already covers the cases that production React Native apps actually hit. The few cases it doesn't cover — global theme, app-wide auth state, cart that follows the user across tabs — are handled inside the scaffold's AppProvider, not generated per-screen.
The benefits for AI generation specifically:
- Generation determinism. Two different prompts that produce similar features produce similar code. There's no creative reach into the prop-types of a Zustand slice that the next prompt won't understand.
- Smaller prompt surface. The system prompt is shorter when the model only needs rules for one state library, not a decision tree between three.
- Editability. Users who don't know React Native can read
useStateanduseQueryafter fifteen minutes with the React Native docs. They can't read a custom Redux slice without a tutorial. - Production-readiness. The same code that renders in a preview iframe with the mock adapter ships to TestFlight against Supabase, with one environment-variable change. There's no "demo code" vs. "real code" split.
The cost is that the platform makes a choice the user doesn't get to make. If you're a senior React Native engineer who loves Zustand, you're going to find yourself swapping it in by hand after export. That's a deliberate trade. The user we're optimizing for isn't a senior React Native engineer — they're a founder or PM or designer who wants a working app, not an architecture discussion. For more on how the export pipeline turns AI-generated code into App Store-ready apps, that's a separate piece.
A few questions we get asked
Why not Zustand? It's smaller than Redux.
Size isn't the constraint — uniformity is. Zustand stores can be authored in five different patterns ("vanilla", "slices", "computed", "persist middleware"), and an LLM will reach for whichever pattern was most common in its training data for that specific feature. The result is a codebase where every store looks slightly different. React Query, by contrast, has one obvious pattern: useQuery + useMutation + invalidateQueries. Less surface, less drift.
What about React Native's new use() hook and Suspense for data fetching?
Worth watching, not worth shipping. The platform's runtime preview environment doesn't yet wrap every component in a Suspense boundary cleanly, and the use() hook surface still flickers under reanimated transitions. We'll likely move there once the React Native New Architecture stabilizes more of the rendering path. Until then, React Query's explicit loading/error states map better to the iframe-based preview that RapidNative renders in real time.
Can I add Redux to an exported project? Yes. The exported codebase is plain React Native plus Expo, and you can drop in Redux Toolkit or Zustand the moment you check it out. We don't lock the architecture; we just don't generate with those libraries because they impose costs the average user won't recover.
How to think about state in your own AI-built apps
If you're building or evaluating an AI app builder — ours or anyone else's — the state architecture is one of the more honest signals of how the system thinks. Surface-level questions like "does it generate beautiful UI?" are dominated by the underlying model. The state question is dominated by taste: did the team building the generator make narrow, opinionated choices, or did they let the model do whatever it wanted?
Narrow choices age better. An app that uses useState + React Query + an adapter pattern in 2026 will still be readable in 2030. A randomly-generated mix of Redux slices, Zustand stores, and ad-hoc Context will be a rewrite. The same logic applies whether the code is hand-written or AI-generated; AI just exposes the problem faster.
If you want to see this architecture in action without reading any more about it, open a new project in RapidNative, describe an app with any kind of persistent data — a workout tracker, a habits app, a customer log — and look at what comes out. Every useState is a screen-local decision. Every useQuery is a server-state decision. Every client.from(...) call goes through the same adapter. That's the architecture, generated end to end, from one prompt.
Ready to build your app?
Turn your idea into a production-ready React Native app in minutes.
Free tools to get you started
Free AI PRD Generator
Generate a professional product requirements document in seconds. Describe your product idea and get a complete, structured PRD instantly.
Try it freeFree AI App Name Generator
Generate unique, brandable app name ideas with AI. Get creative name suggestions with taglines, brand colors, and monogram previews.
Try it freeFree AI App Icon Generator
Generate beautiful, professional app icons with AI. Describe your app and get multiple icon variations in different styles, ready for App Store and Google Play.
Try it freeFrequently asked questions
What is RapidNative?
RapidNative is an AI-powered mobile app builder. Describe the app you want in plain English and RapidNative generates real, production-ready React Native screens you can preview, edit, and publish to the App Store or Google Play.
Can I export the code?
Yes. RapidNative generates clean React Native and Expo code that you can export at any time. No lock-in, no proprietary format. Hand it to your developers or keep building inside RapidNative.
Is RapidNative free to use?
Yes. You can build apps on the free plan with no credit card required. Paid plans unlock unlimited AI generations, code export, and direct publishing to the App Store and Google Play.
Do I need to know how to code?
No. Most users build apps by describing what they want in plain English. Developers can drop into the code whenever they want more control, but coding is optional.
How long does it take to build an app?
Most users have a working first screen in under a minute. A full MVP usually takes a few hours instead of the weeks or months traditional development requires.