React SME Cookbook
All FAQs
componentsdecisionsworkflowarchitecturepatternsplanning

Component Decision Workflow

A step-by-step decision workflow for structuring React components. Walk through each question in order -- your answers determine the architecture, patterns, and tools you need before writing any code.


How to Use This Workflow

Start at Step 1 and work through each decision sequentially. Each step builds on the previous one. By the end, you'll have a clear blueprint for your component -- what it renders, how it manages state, where data comes from, and how it communicates with the rest of your app.


Step 1: What Does This Component Render?

Determine the visual output first. Everything else follows from what the user sees.

QuestionIf YesIf No
Does it render static content (text, images, layout)?Keep it simple -- a Server Component with no hooks or stateMove to Step 2
Does it render a list of items?You need .map(), unique key props, and likely data from a parent or APIConsider a single-item component
Does it render other components (composition)?Design the children or slot-based API nowIt's a leaf component -- focus on props
Does it render nothing sometimes (conditional)?Plan your conditional rendering strategy (ternary, &&, early return)It always renders -- simpler path

Key decision: If the component renders only static content with no interactivity, it should be a Server Component (the default in Next.js App Router). Stop here -- you don't need client-side JavaScript.

Related: Components -- composition patterns, props, children | JSX and TSX -- expression rules, fragments, JSX compilation | Conditional Rendering -- ternary, &&, early return patterns | Server Components -- when and why to use them


Step 2: Does It Need Interactivity?

If the user clicks, types, hovers, drags, or otherwise interacts with it -- this is a Client Component.

Interaction TypeWhat You NeedExample
Button clicks / togglesonClick handler, possibly useStateLike/unlike, expand/collapse
Text inputControlled input with useState + onChangeSearch bar, form field
Hover / focus effectsCSS-only (preferred) or onMouseEnter/onFocusTooltip, dropdown trigger
Drag and dropThird-party library (dnd-kit) or Drag APISortable list, kanban board
Keyboard shortcutsuseEffect + addEventListener or custom hookCmd+K search, Escape to close

Key decision: Add "use client" at the top of the file only when the component genuinely needs browser APIs, hooks, or event handlers. Keep the client boundary as low in the tree as possible.

Related: Event Handling -- synthetic events, handler patterns | Mouse Events -- click, double-click, hover | Keyboard Events -- keydown, key combos | Focus Events -- focus, blur, tab navigation | useKeyboardShortcut -- custom hook for key combos


Step 3: What State Does It Own?

Determine what data the component manages internally vs. what comes from outside.

State ScenarioRecommended Approach
No state -- pure displayProps only. Stateless components are easier to test and reuse.
Simple toggle/counter (1-2 values)useState -- one call per independent value
Related fields that change together (form with 3+ fields)Single useState with an object, useReducer for complex transitions, or a Zustand store if the same state may need to be read/written from sibling components
Complex state machine (multi-step wizard, drag state)useReducer with explicit actions, a state machine library, or a Zustand store with action methods when steps span multiple components
Derived values (filtered list, computed total)Compute during render -- do not store in separate state

Key decision: If you're reaching for more than 3-4 useState calls in one component, stop and consider useReducer or extracting a custom hook.

Related: useState -- updater functions, lazy initialization, batching | useReducer -- action-based state for complex logic | Zustand Setup -- lightweight store for cross-component state | Typing State -- typing complex state shapes | State Machines -- finite state patterns for UI


Step 4: Where Does the Data Come From?

Data fetching strategy depends on whether you're in a Server or Client Component.

Data SourceBest ApproachAvoid
Database / ORMFetch directly in an async Server ComponentExposing DB queries in client code
Your own APIServer Action or fetch in Server ComponentuseEffect + fetch in a Client Component (causes waterfalls)
Third-party APIServer Component fetch, or SWR/TanStack Query for client-sideRaw useEffect + fetch without caching
URL params / search paramsuseParams() / useSearchParams() in Client Component, or params prop in Server ComponentParsing window.location manually
Parent componentProps -- always the simplest pathGlobal state for parent-child communication
User inputControlled state (useState + onChange)Uncontrolled refs for values you need to validate or display

Key decision: Default to Server Components for data fetching. Only fetch on the client when you need real-time updates, user-initiated refreshes, or infinite scroll.

Related: SWR Basic Fetching -- client-side fetching with caching | SWR Revalidation -- stale-while-revalidate strategies | useEffect -- when you must fetch client-side | Next.js Data Fetching -- Server Component data patterns | Typing API Responses -- type-safe fetch results


Step 5: How Does It Communicate with Other Components?

Determine how data and events flow between this component and the rest of the app.

Communication PatternWhen to UseExample
Props downParent passes data to child<UserCard name={user.name} />
Callbacks upChild notifies parent of an event<SearchInput onSearch={handleSearch} />
ContextMany descendants need the same data (theme, auth, locale)useContext(ThemeContext)
Global store (Zustand)Cross-cutting state shared by unrelated componentsCart count in header + checkout page
URL stateState that should survive refresh or be shareableFilters, pagination, active tab
Server ActionsForm submissions or mutations that hit the server<form action={submitForm}>

Key decision: Start with props and callbacks. Reach for Context when you're drilling props through 3+ levels. Reach for Zustand when multiple unrelated components need the same state.

Related: Composition -- slots and layout patterns | Context Patterns -- when and how to use Context | Zustand Setup -- store creation and usage | Context vs. Zustand -- choosing the right tool | Server Actions -- mutations from the client | Controlled vs. Uncontrolled -- component API design


Step 6: Does It Handle User Input (Forms)?

Forms have their own decision tree. The answer depends on complexity and where validation happens.

Form ComplexityRecommended Stack
1-3 fields, simple submitNative <form action={serverAction}> + useActionState + useFormStatus
4-8 fields with validationreact-hook-form + zod schema + shadcn/ui form components
Multi-step wizardreact-hook-form + useReducer for step state + zod per-step schemas
File uploads<input type="file"> + FormData + server action or presigned URL
Real-time validation (as-you-type)react-hook-form mode "onChange" + zod
Optimistic updatesuseOptimistic hook for instant feedback while server processes

Key decision: For simple forms, avoid libraries entirely -- React 19's built-in useActionState and useFormStatus handle pending state, errors, and progressive enhancement with zero dependencies. Reach for react-hook-form + zod when you need complex validation or many fields.

Related: Forms -- controlled vs. uncontrolled, form submission | Form Decision Checklist -- 20 questions to ask before building a form | Gherkin Form Decision Checklist -- test-driven form planning with BDD | React Hook Form + Zod -- schema-validated forms | Form Patterns (Basic) -- simple form recipes | Form Patterns (Complex) -- multi-step and dynamic fields | Controlled vs. Uncontrolled -- when to use each | Form Events -- onChange, onSubmit, onBlur


Step 7: What TypeScript Types Does It Need?

Define your types before writing the component body. This catches design mistakes early.

What to TypeHow
Props interfaceinterface UserCardProps { name: string; email: string; onEdit?: () => void; }
State shapeuseState<User | null>(null) -- explicit generic for complex types
Event handlersReact.MouseEventHandler<HTMLButtonElement> or inline (e: React.ChangeEvent<HTMLInputElement>) => void
ChildrenReact.ReactNode for anything renderable, React.ReactElement for a single element
RefReact.Ref<HTMLInputElement> for forwarded refs
Discriminated unionstype ButtonProps = { variant: "link"; href: string } | { variant: "button"; onClick: () => void }

Key decision: Always type props as an interface (not inline) when the component has more than 2 props. Export the interface so consumers can extend or reference it.

Related: Typing Props -- interfaces, optional props, rest props | Typing Events -- event handler types | Typing Refs -- ref types and forwarding | Discriminated Unions -- type-safe variant props | Generics -- generic component typing | TypeScript + React Basics -- getting started with TS in React


Step 8: How Should It Be Exported and Organized?

File structure and export style affect tree-shaking, DevTools clarity, and team conventions.

QuestionRecommendation
Is it a Next.js page/layout/route file?export default function PageName() -- function declaration, always named
Is it a shared/reusable component?Named export: export function UserCard() -- tree-shakeable, multiple per file allowed
Is it a one-off helper in the same file?Plain function declaration below the main export (hoisting makes order flexible)
Does it need static properties?Two-step: const Tabs = () => {...}; Tabs.Panel = TabPanel; export default Tabs;
Arrow or function declaration?Function declarations for components (hoisted, clean generics). Match your team's lint rules.

Key decision: Be consistent within a file. If the main component is a function declaration, helpers should be too. Mixing styles signals intentional difference where there is none.

Related: Function Component Syntax -- all 9 syntax variations with trade-offs | React & Next.js Architecture Decisions -- ranked choices for common architecture questions


Step 9: Does It Need Performance Optimization?

Optimize only when you measure a problem. Premature optimization adds complexity with no benefit.

SymptomToolWhen to Apply
Child re-renders when parent state changesReact.memo()Only if the child is expensive to render and props haven't changed
Expensive computation on every renderuseMemoOnly if the computation takes >1ms and inputs rarely change
Callback causes child to re-renderuseCallbackOnly when passing callbacks to memoized children
Large list renderingVirtualization (react-window, tanstack-virtual)Lists with 100+ items visible in the DOM
Heavy component on initial loadReact.lazy() + Suspense, or next/dynamicModals, charts, editors -- anything not visible on first paint
Layout shift from imagesnext/image with width/height or fillEvery image should declare dimensions

Key decision: Profile first with React DevTools Profiler or Chrome Performance tab. Never add memo, useMemo, or useCallback without evidence of a performance problem.

Related: Performance Patterns -- memoization, virtualization, code splitting | useMemo -- when memoization helps (and when it hurts) | useCallback -- stable references for child props | React Compiler -- automatic memoization in React 19 | Suspense -- lazy loading and streaming


Step 10: How Will It Be Tested?

Choose your testing strategy based on component complexity and risk.

Component TypeTesting Approach
Pure display (no state)Snapshot test or skip -- low risk, high churn
Interactive (clicks, inputs)Integration test with React Testing Library -- simulate user actions
Form with validationTest the happy path and each validation rule -- use Gherkin scenarios as specs
Data fetchingMock the API, test loading/error/success states
Custom hookTest with renderHook from React Testing Library
Complex workflow (multi-step)End-to-end test with Playwright or Cypress

Key decision: Test behavior, not implementation. If you're asserting on internal state or specific DOM structure, the test will break on every refactor. Assert on what the user sees and does.

Related: Gherkin Form Decision Checklist -- write BDD specs before coding | Gherkin to Code -- translating Gherkin scenarios to test code | Gherkin to Deploy Pipeline -- from spec to CI/CD


Step 11: Does It Need Accessibility?

Accessibility is not optional -- it's a baseline requirement. Plan it into the component from the start.

ElementMinimum Requirements
ButtonsUse <button>, not <div onClick>. Add aria-label if icon-only.
InputsAssociate with <label> via htmlFor. Add aria-describedby for error messages.
Modals / dialogsTrap focus, return focus on close. Use role="dialog" and aria-modal="true".
Imagesalt text always. Decorative images get alt="".
NavigationUse <nav>, <main>, <aside> landmarks. Ensure keyboard navigation works.
Dynamic contentUse aria-live regions for content that updates without page reload.
Color contrastMinimum 4.5:1 for normal text, 3:1 for large text (WCAG AA).

Key decision: Use semantic HTML elements first (<button>, <nav>, <dialog>). Only reach for ARIA attributes when native semantics don't cover your use case.

Related: Form Accessibility -- accessible form patterns and error display | Form Error Display -- inline errors, summary, aria-live


Step 12: Where Does It Live in the File Structure?

Organize by feature, not by type. Co-locate related files.

File TypeLocationExample
Page componentapp/[route]/page.tsxapp/dashboard/page.tsx
Layoutapp/[route]/layout.tsxapp/dashboard/layout.tsx
Shared UI componentcomponents/[name].tsxcomponents/user-card.tsx
Feature-specific componentapp/[feature]/components/[name].tsxapp/dashboard/components/stats-chart.tsx
Custom hookhooks/[name].ts or co-located with its componenthooks/use-toggle.ts
TypesCo-locate in the same file, or types/[domain].ts for shared typestypes/user.ts
Server Actionsapp/[feature]/actions.tsapp/dashboard/actions.ts

Key decision: If a component is used by only one page, co-locate it with that page. If it's used by 2+ pages, promote it to components/. Don't create folders for a single file.

Related: Next.js App Router -- file conventions, pages, and route segments | Next.js Layouts -- nested layouts, templates, and route groups


Quick-Reference Flowchart

Start
  |
  v
1. What does it render? -----> Static only? --> Server Component (done)
  |
  v
2. Needs interactivity? -----> No? --> Server Component with props (done)
  |                                     
  v (yes: add "use client")
3. What state does it own? --> None? --> Stateless Client Component
  |                           Simple? --> useState
  |                           Complex? --> useReducer or custom hook
  v
4. Where does data come from? --> Server? --> Fetch in parent Server Component, pass as props
  |                               Client? --> SWR or TanStack Query
  v
5. How does it communicate? ---> Parent-child? --> Props + callbacks
  |                              Cross-cutting? --> Context or Zustand
  v
6. Is it a form? -------------> Simple? --> useActionState
  |                              Complex? --> react-hook-form + zod
  v
7. Define TypeScript types
  v
8. Choose export style
  v
9. Optimize only if measured
  v
10. Plan testing strategy
  v
11. Add accessibility
  v
12. Place in file structure
  v
Done -- start coding