How Our AI Understands Complex UI Layouts and Component Hierarchies

SA

By Suraj Ahmed

10th Sep 2026

Last updated: 10th Sep 2026

How Our AI Understands Complex UI Layouts and Component Hierarchies

A modern mobile app is not a single file. A single "settings" screen might touch a screen component, three or four presentational components, a shared header, a themed button, a hook that reads user preferences, a route file that wires it into a tab, and a NativeWind theme file that decides what "primary" even means. When a user tells our AI "add a new setting to toggle push notifications", the model has to move through that whole tree — read the right files, know which component owns the row layout, understand where "primary" resolves to a real color, and update the code without breaking any of the parent-child relationships.

This is the part of AI code generation that gets least discussed publicly and matters the most in practice. Getting one screen right is a demo. Getting the twentieth edit on the same nested layout right is a product. This post walks through exactly how RapidNative's AI reasons about complex UI layouts and component hierarchies — the virtual file system, the tool interface, the mapping from a rendered element back to a source line, and the invariants that keep a component tree consistent across generations.

Nested React Native component hierarchy on a designer's screen Every mobile screen is a tree of nested components — AI code generation only works if the model can reason across that whole tree, not just the leaves — Photo by Alvaro Reyes on Unsplash

The problem: a component tree is not a file

Before getting into the architecture, it helps to name the problem precisely. When you ask an AI to "make the header sticky and add a badge next to the user's name", the request looks like a single edit but the actual change might span:

  • The screen file that renders the header
  • A shared Header.tsx component several folders away
  • A theme file that owns the badge color
  • A layout file (_layout.tsx in Expo Router) that decides whether the header can be sticky at all

Traditional AI code assistants that operate on one file at a time miss most of that. They see the file the cursor is in, guess at the rest, and produce code that compiles but doesn't match the app's real conventions. That's the fundamental failure mode we spend most of our engineering time preventing — and the way we prevent it is by giving the model a real, navigable representation of the entire project, not just a prompt window.

A virtual file system, not a chat window

The first thing our AI does when a generation starts is materialize the current project as a virtual file system in memory. In src/lib/coding-agent/vfs.ts we keep a session-scoped Map<sessionId, Map<path, content>>; the moment your request arrives, we seed it with every file we hold for the project (fetched from the files table in Supabase, ordered by path) and hand the AI a set of tools to read, list, edit, move, and write inside it.

Those tools are boring on purpose. The agent gets list_files, read_file, edit_file (exact-string replacement, no regex), move_file, and a two-step begin_write_file + write_file_content pair for creating new files. There is no vector database, no embedding search, no clever retrieval. The agent navigates the codebase the way an engineer does — list_files to see the layout, read_file on the specific components it cares about, edit_file for a targeted change. Boring tools compose into non-boring behavior. This is the pattern behind every modern coding agent that actually works, and it's what makes cross-file component-tree edits tractable.

The reason a VFS matters more than it sounds like it should: the storage adapter is an interface (IFileStorage), and we have three implementations of it. MemoryFileStorage is used inside the API route for one-shot generations. FsFileStorage writes to a real directory and drives our Node-based test harness. LifoFileStorage mirrors changes into the live editor's sandbox so the preview iframe hot-reloads while the model is still typing. Every one of them exposes the same interface, so the same agent code — same tools, same prompts, same model — produces the same behavior across a browser session, an integration test, and a debug harness. That's how we get regressions to be reproducible instead of legend.

Skills give the model the conventions before it starts editing

Reading files gets you the shape of the code. It doesn't tell you the rules — which button component to use, what a screen file is supposed to look like, how to declare a new route in Expo Router, or how our specific theme system wires colors. Those rules live in a set of skill documents that ship with each project template.

On the first message of a new project, the agent's system prompt includes every skill inline. On follow-up messages we only include the core skills and expose list_skills / search_skills / read_skills (src/lib/coding-agent/skill-tools.ts) so the model can pull in exactly the ones it needs. The design pattern is easy to see once you notice it: give the model the full playbook once, then let it retrieve chapters on demand. This keeps prompt sizes down, keeps prefix cache hits high, and — most importantly — means the model is reading actual product conventions before it touches a component, not making them up from base training.

The practical effect on component hierarchies is enormous. When the model is told to "add a settings row for notifications", it doesn't invent a new row primitive. It reads the component-patterns skill, finds the existing SettingsRow component, and reuses it — parents and children stay consistent because the model was shown the parent-child grammar of this specific codebase before it started writing.

Developer thinking through nested components on a whiteboard Skill documents give the AI the parent-child grammar of a specific codebase — the same way a new engineer reads the style guide before touching production code — Photo by LinkedIn Sales Solutions on Unsplash

The generation loop: tool calls, not text

Inside src/app/api/user/ai/generate-v3/route.ts, the actual generation is an agentic tool-calling loop built on the Vercel AI SDK v6. The route wires together the file tools, the database tools, the skill tools, and a couple of platform action tools, then calls streamText() with a hard cap of 40 tool-calling steps (stopWhen: [stepCountIs(40), hasToolCall('ask_question')]).

Two details of the loop matter for component hierarchies specifically.

The two-step write is streaming-native. New files are created by calling begin_write_file(path) and then streaming content into write_file_content chunk-by-chunk. This isn't a stylistic choice — it's what lets the preview iframe start rendering a new screen before the entire file has been written. When you see a screen materialize live during generation, that's the two-step write feeding partial JSX into the live Metro bundler via LifoFileStorage, and the JSX fixer (src/lib/coding-agent/jsx/jsxFixer.ts) closing any unbalanced tags on the way through so the intermediate state parses.

The stop condition is intentional. Forty steps sounds like a lot, but many real edits use ten or twenty when you count list_files, several read_file calls to build a mental model of the component tree, one or two edit_files, and a couple of db_migration_new calls if the change touches data. The hard cap keeps runaway loops from burning credits; the ask_question short-circuit lets the model pause and ask you which parent component to use if the ambiguity is genuine.

The system prompt is not a suggestion — it's a set of invariants

We do the model no favors by pretending "please be careful with component hierarchies" is enough. The system prompt in src/lib/coding-agent/system-prompt.ts is layered:

  1. A base set of hardcoded rules (file-tool contracts, database-tool contracts, import rules, per-turn caps)
  2. The set of skills relevant to the request
  3. An optional plan context from a prior reasoning pass
  4. URL context extracted from the user's message
  5. Icon and navigation nudges when the current state calls for them

The base rules are the part that keeps component hierarchies from drifting. Per-turn caps limit new screens to 5, new files to 12, new components to 3, edits to 8. These are enforced by the tools themselves, not by prompt language — the model can want to add ten screens in one turn and the tool will refuse. That refusal is what forces the agent to make focused, bounded changes to the tree, which is exactly the class of edit that a mobile-app codebase can absorb without decoherence.

Mapping the rendered DOM back to the source: data-bx-path

The most visible part of this system to end users is the visual editor: click any element in the preview and describe what you want changed. It looks simple. It is not.

Every JSX element the AI generates includes a data-bx-path="path/to/file.tsx:ComponentName" attribute on its root. When you click that element in the preview iframe, we walk up the React Fiber tree from the DOM node you clicked, looking for the nearest fiber whose memoizedProps carries a data-bx-path. We split the value on the colon, resolve the file path against the project's Metro root (the same file can appear as /app/(tabs)/index.tsx in one view and mobile/app/(tabs)/index.tsx in another — the resolver in src/modules/studio/components/custom/editor/SimpleSandboxEditor.tsx matches by suffix), and dispatch a Redux action that updates editor.selectedLayer.path.

That path becomes the anchor for everything downstream. The Monaco editor scrolls to the file. The AI, on the next turn, is told which layer you selected — so when you type "make this row taller", "this" refers to a specific SettingsRow inside a specific screen, not a guess. Two mistakes lurking in this design have caused real bugs and are worth naming out loud:

  • Two path roots coexist. data-bx-path is Metro-root relative; artboard keys in Redux are workspace relative. Comparisons must be suffix-wise in both directions. This mismatch has produced three separate bugs so far, all of them looking like "the AI edited the wrong component."
  • data-bx-path is emitted by the model, not injected by a post-processor. It is part of the JSX the AI writes. The system prompt guarantees it. If you ever see an element the point-and-edit can't select, the almost-certain cause is that the model dropped the attribute on that element — not a bug in the resolver.

The rendered tree, the fiber tree, and the source tree line up because of a single attribute that spans all three.

Mobile app preview showing a live component being edited Clicking a rendered element and describing an edit is the visible tip of a much deeper mechanism — a single attribute per element links the DOM, the Fiber tree, and the source file — Photo by NordWood Themes on Unsplash

Redux is the tree's second copy

The editor holds a parallel representation of the project as Redux state. In src/modules/editor/store/slices/editorSlice.ts the artboards array is keyed by file path — each artboard is a { path, x, y, width, height, content, route?, previewNotice? } record, and the current selection is a single selectedLayer with a path field derived from data-bx-path.

This is deliberately not a full mirror of the component tree — trying to keep an in-memory model of every nested JSX node in sync with a stream of AI edits is a losing battle. Instead, the Redux state models the outermost boundaries of the tree (which files are open, which route each represents, what the currently selected leaf is) and defers to the actual rendered React tree for everything inside. The Fiber tree in the preview iframe is the source of truth for the DOM-to-source mapping; Redux is the source of truth for editor-level intent.

That split falls out of a hard-won lesson: modeling the tree twice invites the two copies to disagree. Where possible, we do not model it twice — we point at the copy that already exists in React.

Streaming, HMR, and why the preview shows the tree building live

The last piece of the puzzle is the preview pipeline. Generated files are written into LifoFileStorage, which sits inside a Lifo Sandbox VM running browser-metro — a Metro bundler that runs in the browser. File watchers in the VM detect writes, incrementally rebuild the affected modules, and push a hot update to the iframe.

For an existing screen this is standard React Native HMR. For a brand-new screen — a route that didn't exist when the running bundle loaded — one document reload is required, because HMR has no accept boundary for a module nothing imports. This is one of those facts that is obvious in retrospect and invisible until you've spent a day chasing "hot update (0 modules)" in the console. We coordinate route reloads through a small queueRouteAssert() helper with a 1.5-second gap between assertions, because repeated mid-stream reloads are destructive — each restart discards the partial render, and in production this produced the "generated a screen and now the preview is blank" bug that we fixed and never want back.

The important consequence for component hierarchies is this: users see the tree materialize incrementally, not all at once at the end. That in turn gives the model a fast feedback signal — screens that don't parse are visible immediately, and the JSX fixer or the follow-up turn can correct them before you've read a single line of code.

What this means for a user

Everything above is invisible to a user asking to "add a Notifications section to Settings". They type the request, they see the section appear in the preview, they click it if they want it taller, they type "add a chevron on the right", it gets a chevron. The interesting property of the system is that under the hood, this two-turn interaction touched a shared component (SettingsRow), a screen file (app/(tabs)/settings.tsx), and a theme constant (components/theme/colors.ts) — and the whole hierarchy stayed consistent because:

  • The VFS gave the model the whole project to read from, not just the current file
  • Skill documents told the model which existing components to reuse
  • Per-turn caps kept the edit bounded
  • data-bx-path anchored the second turn to the exact component the user clicked
  • The JSX fixer kept intermediate streaming states parseable
  • The Redux/Fiber split kept editor state and rendered state from disagreeing

None of these pieces are novel individually. What's novel is the composition — and how much of the model's job we've moved out of the prompt and into the tools, the file system, and the invariants that surround them.

Related reading

If you want to go deeper on adjacent pieces of the same pipeline, we've written about the multi-file AI code generation architecture, how point-and-edit works under the hood, and the streaming architecture behind live preview. Each one covers a slice of the same system from a different angle.

Try it

If you want to see the mechanism in action rather than read about it, the fastest path is to describe an app in a sentence and watch the tree build itself. Start building on RapidNative — the free tier includes 20 credits, which is enough to feel the full generation loop end-to-end without paying anything. If you're evaluating for a team, the pricing page has the collaboration limits, and the PRD-to-app flow is worth trying if your team already writes product specs.

Understanding how AI models reason about deeply nested component hierarchies is the difference between a demo that generates one screen and a product that keeps a real codebase coherent across a hundred edits. The mechanism is not magic; it's a virtual file system, a small set of well-designed tools, and a handful of invariants — and every one of them is worth getting right.

Start now

Ready to build your app?

Turn your idea into a production-ready React Native app in minutes.

Free tools to get you started

Questions

Frequently 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.