react-screen-architecture
Skill18 starsChanged 5 months agoMITView on GitHub
Screen-level architecture for React SPAs - re-render behavior, state placement and co-location, derived state, when useEffect is allowed and what to use instead, Suspense boundaries, lists and keys, state that survives a reload, leaving a page with unsaved changes, responsive layout through the Figma breakpoints, keyboard-accessible drag and drop, automatic memoization by the React Compiler, hover and touch, focus and keyboard handling, document titles, and animations. This skill is about HOW screens are structured, not how individual components are built (see react-ui-components) or how the Root/Screen/view-model split works (see react-presentation-mvi). Use this skill whenever structuring a screen, deciding where state lives, adding a useEffect, fixing unnecessary re-renders, adding memoization, rendering lists, making a layout responsive, or adding animations. Trigger on phrases like "React Compiler", "drag and drop", "dnd-kit", "re-render", "performance", "useEffect", "useMemo", "useCallback", "memo", "derived state", "lift state", "Suspense", "key prop", "long list", "virtualize", "responsive", "breakpoint", "mobile layout", "matchMedia", "hover", "focus", "keyboard", "document title", "unsaved changes", "animation", "transition", or "prefers-reduced-motion".
In this repository67 other files it gives its agents.
- agent-onboardingSkill
- android-backgroundSkill
- android-cliSkill
- android-compose-architectureSkill
- android-compose-componentsSkill
- android-data-layerSkill
- android-di-koinSkill
- android-error-handlingSkill
- android-integration-testingSkill
- android-jacoco-setupSkill
- android-jvm-test-coverageSkill
- android-module-structureSkill
- android-navigationSkill
- android-presentation-mviSkill
- android-preview-figma-verifySkill
- android-ui-screenshot-verifySkill
- android-ui-testingSkill
- android-ui-verifySkill
- android-unit-testingSkill
- android-version-catalogSkill
- architecture-decision-recordSkill
- careful-planningSkill
- compose-adaptive-layoutsSkill
- figma-to-android-vectorSkill
- finalizeSkill
- git-commitSkill
- handoffSkill
- kmp-compose-architectureSkill
- kmp-compose-componentsSkill
- kmp-data-layerSkill
- kmp-di-koinSkill
- kmp-error-handlingSkill
- kmp-integration-testingSkill
- kmp-migration-analyzerSkill
- kmp-module-structureSkill
- kmp-navigationSkill
- kmp-presentation-mviSkill
- kmp-ui-testingSkill
- kmp-unit-testingSkill
- kotlin-coroutinesSkill
- kotlin-flowsSkill
- kotlin-mutation-testingSkill
- openspec-artifact-review-lightSkill
- openspec-artifact-reviewSkill
- project-breakdownSkill
- react-data-layerSkill
- react-error-handlingSkill
- react-integration-testingSkill
- react-presentation-mviSkill
- react-project-structureSkill
- react-routingSkill
- react-securitySkill
- react-server-stateSkill
- react-ui-componentsSkill
- react-unit-testingSkill
- speckit-parallelize-agent-teamsSkill
- speckit-parallelize-subagentsSkill
- spring-api-layerSkill
- spring-di-configurationSkill
- spring-integration-testingSkill
- The first 60 shown.
The file
Show its rules and text
We split this skill into the rules it gives an agent
14 rules. About 0 are common, 7 are distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈7
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 21
If the compiler skips a component (it bails out on code that breaks the Rules of React), fix the code instead of adding manual memoization.
- Distinctive: line 29
If a value can be computed from props, state or query data, compute it during render:
- Very specific: line 40
Use TanStack Query's select option for derivations of server data that several components share (see react-server-state).
- Distinctive: line 60
Every subscription has a cleanup.
- Very specific: line 70
Don't wrap every component in its own Suspense.
- Very specific: line 97
Don't use localStorage to keep screen state unless the user expects it to persist (like a draft).
- Very specific: line 112
Use the breakpoints from the theme, tablet (640 px) and desktop (1024 px), which come from Figma Foundations.
- Very specific: line 113
Read the window size in JavaScript (matchMedia through useSyncExternalStore) only when the component tree itself differs, for example a …
- Distinctive: line 114
Every interaction works with touch, mouse and keyboard.
- Distinctive: line 116
Test layouts at the breakpoint boundaries (one pixel below and at each breakpoint).
- Very specific: line 122
Use semantic elements (main, nav, h1–h3, button, a, ul) so keyboard and screen reader navigation work without extra code.
- Distinctive: line 126
After a destructive action removes the focused element (deleting a row), move focus somewhere sensible, such as the list heading.
- Very specific: line 133
Don't animate width, height, top or left.
- Distinctive: line 134
Don't drive animations with React state updated every frame.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
React Screen Architecture
Core Principle
Components render state and forward user input. Logic lives in reducers, view-model hooks, utils/ and the API layer. A Screen contains no business rules, no data transformation beyond formatting for display, and as few effects as possible.
Re-renders
A component re-renders when its state changes, when its parent re-renders, or when a context it reads changes. Re-rendering is cheap. What makes it expensive is a large subtree re-rendering on every keystroke or on every frame.
- Keep state as low in the tree as possible. State used by one component lives in that component. Lift it only as far as the nearest component that needs it.
- No manual memoization. The project runs the React Compiler, which memoizes components, values and callbacks automatically. Don't write
useMemo,useCallbackormemo. The one exception is a value that a non-React library compares by identity and the compiler can't see. Add a short comment saying why. - If the compiler skips a component (it bails out on code that breaks the Rules of React), fix the code instead of adding manual memoization.
- Inputs don't re-render the screen. Forms use React Hook Form's uncontrolled inputs (see react-presentation-mvi). A controlled input is only for a value the rest of the screen reacts to, like a search box.
- Context is not a state store. A context value that changes often re-renders every consumer. App-wide contexts hold stable objects (the QueryClient, i18n), not changing UI state.
Derived State
If a value can be computed from props, state or query data, compute it during render:
// Good
const visibleNotes = filterByQuery(notes, query)
// Bad: an extra render, and a frame where the two values disagree
const [visibleNotes, setVisibleNotes] = useState(notes)
useEffect(() => setVisibleNotes(filterByQuery(notes, query)), [notes, query])Use TanStack Query's select option for derivations of server data that several components share (see react-server-state).
Effects
useEffect is for synchronizing with something outside React. Before writing one, check this table:
| You want to | Use instead of an effect |
| Fetch data | TanStack Query (useSuspenseQuery, useQuery) and route loaders |
| Compute a value from other values | compute it during render |
| React to a click or a submit | the event handler, or onAction |
| Navigate after a mutation | the mutation's onSuccess calling a callback |
| Reset state when an ID changes | key={id} on the component |
| Notify a parent about a change | call the parent's callback in the event handler |
Valid uses: subscribing to a browser API (resize, online/offline, matchMedia), setting document.title, managing focus after something appears, integrating a non-React library.
- Extract every effect into a named custom hook (
useDocumentTitle,useOnlineStatus) incoreor the feature'shooks/, so Screens don't contain raw effects. - Every subscription has a cleanup.
- For subscribing to an external store, prefer
useSyncExternalStoreoveruseEffect+useState. StrictModeruns effects twice in development. If that breaks something, the effect is missing a cleanup.
Suspense Boundaries
- The route is the default boundary. Its
pendingComponentshows while the loader and the code chunk load (see react-routing). - A secondary section that loads on its own (a sidebar, a "related items" panel) gets its own
<Suspense fallback={...}>around a component that callsuseSuspenseQuery, so it doesn't block the page. - Don't wrap every component in its own
Suspense. Several spinners that appear and disappear one by one look worse than one. - Fallbacks are skeletons sized like the content, so the layout doesn't jump when data arrives.
Lists
{state.notes.map((note) => (
<NoteCard key={note.id} note={note} />
))}keyis a stable ID from the data. Never the array index for lists that can be reordered, filtered or edited, because React then reuses the wrong component state.- Long lists (hundreds of rows or more) are virtualized with TanStack Virtual. Don't virtualize short lists. It breaks the browser's find-in-page and adds complexity.
- Lists that load more pages use
useInfiniteQuery(see react-server-state).
Reloads and Leaving a Page
The web has no process death, but every reload, every new tab and every shared link starts from the URL.
- Survives a reload: path and search params. Put filters, sort order, the selected tab and the open item there.
- Doesn't survive a reload: reducer state, form input, scroll position within the page. That's fine for most state.
- Unsaved changes: a form with significant user input uses TanStack Router's
useBlockerto ask before navigating away, including on tab close. - Writing to the URL on every keystroke (a search box) uses
navigate({ search, replace: true }), so each keystroke doesn't become a history entry. - Don't use
localStorageto keep screen state unless the user expects it to persist (like a draft). Never store tokens or personal data there (see react-security).
Responsive Layout
Layout differences are CSS, not JavaScript:
<div className="grid grid-cols-1 gap-4 tablet:grid-cols-2 desktop:grid-cols-3">The layouts come from the Figma frames at the three sizes: phone 390, tablet 834, desktop 1440. Compare all three before building a screen. Tablet landscape uses the desktop layout.
- Tailwind is mobile-first: unprefixed classes are for the smallest size, and breakpoint prefixes add to them.
- Use the breakpoints from the theme,
tablet(640 px) anddesktop(1024 px), which come from Figma Foundations. Never Tailwind's defaults, and never a width in a media query ormatchMediacall outside the theme. - Read the window size in JavaScript (
matchMediathroughuseSyncExternalStore) only when the component tree itself differs, for example a side navigation on large screens vs a bottom bar on small ones. Put that hook incore/ui. - Every interaction works with touch, mouse and keyboard. Nothing is only reachable on hover.
- Drag & drop uses dnd-kit with its pointer, touch and keyboard sensors, plus screen reader announcements for pick up, move, drop and cancel. Keyboard users move items with Space, the arrow keys and Escape. Don't add another way to move items (like a "Move to…" menu) unless Figma designs it. Hover styles use the
hover:variant, which Tailwind v4 already limits to devices that can hover. - Test layouts at the breakpoint boundaries (one pixel below and at each breakpoint).
Focus, Keyboard and Page Title
- Use semantic elements (
main,nav,h1–h3,button,a,ul) so keyboard and screen reader navigation work without extra code. Each page has exactly oneh1. - Interactive elements are
buttonora, never adivwithonClick. - Dialogs, menus and popovers come from
core/ui(Radix underneath), which handles focus trapping, Escape and returning focus. - Each page sets a translated
document.titlethrough a shared hook. - After a destructive action removes the focused element (deleting a row), move focus somewhere sensible, such as the list heading.
Animations
- Animate
transformandopacitywith CSS transitions or Tailwind'stransition-*classes. They run on the compositor without layout or re-renders, likegraphicsLayerin Compose. - Don't animate
width,height,toporleft. - Don't drive animations with React state updated every frame.
- Respect reduced motion: use the
motion-safe:andmotion-reduce:variants.
Checklist: Structuring a Screen
- Each piece of state has one owner: Query cache, URL, reducer, form, or local component
- No state that can be computed during render
- Every
useEffectsyncs with something outside React, sits in a named custom hook, and has a cleanup - No manual memoization (the React Compiler handles it)
- Lists keyed by stable IDs
- Shareable and reload-relevant state in search params
- Responsive through the theme's
tablet/desktopclasses, matching the Figma frames, works with touch and keyboard (drag & drop included) - One
h1, semantic elements, a translated page title - Animations use
transform/opacityand respect reduced motion
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

