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.
| Question | If Yes | If No |
|---|---|---|
| Does it render static content (text, images, layout)? | Keep it simple -- a Server Component with no hooks or state | Move to Step 2 |
| Does it render a list of items? | You need .map(), unique key props, and likely data from a parent or API | Consider a single-item component |
| Does it render other components (composition)? | Design the children or slot-based API now | It'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 Type | What You Need | Example |
|---|---|---|
| Button clicks / toggles | onClick handler, possibly useState | Like/unlike, expand/collapse |
| Text input | Controlled input with useState + onChange | Search bar, form field |
| Hover / focus effects | CSS-only (preferred) or onMouseEnter/onFocus | Tooltip, dropdown trigger |
| Drag and drop | Third-party library (dnd-kit) or Drag API | Sortable list, kanban board |
| Keyboard shortcuts | useEffect + addEventListener or custom hook | Cmd+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 Scenario | Recommended Approach |
|---|---|
| No state -- pure display | Props 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 Source | Best Approach | Avoid |
|---|---|---|
| Database / ORM | Fetch directly in an async Server Component | Exposing DB queries in client code |
| Your own API | Server Action or fetch in Server Component | useEffect + fetch in a Client Component (causes waterfalls) |
| Third-party API | Server Component fetch, or SWR/TanStack Query for client-side | Raw useEffect + fetch without caching |
| URL params / search params | useParams() / useSearchParams() in Client Component, or params prop in Server Component | Parsing window.location manually |
| Parent component | Props -- always the simplest path | Global state for parent-child communication |
| User input | Controlled 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 Pattern | When to Use | Example |
|---|---|---|
| Props down | Parent passes data to child | <UserCard name={user.name} /> |
| Callbacks up | Child notifies parent of an event | <SearchInput onSearch={handleSearch} /> |
| Context | Many descendants need the same data (theme, auth, locale) | useContext(ThemeContext) |
| Global store (Zustand) | Cross-cutting state shared by unrelated components | Cart count in header + checkout page |
| URL state | State that should survive refresh or be shareable | Filters, pagination, active tab |
| Server Actions | Form 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 Complexity | Recommended Stack |
|---|---|
| 1-3 fields, simple submit | Native <form action={serverAction}> + useActionState + useFormStatus |
| 4-8 fields with validation | react-hook-form + zod schema + shadcn/ui form components |
| Multi-step wizard | react-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 updates | useOptimistic 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 Type | How |
|---|---|
| Props interface | interface UserCardProps { name: string; email: string; onEdit?: () => void; } |
| State shape | useState<User | null>(null) -- explicit generic for complex types |
| Event handlers | React.MouseEventHandler<HTMLButtonElement> or inline (e: React.ChangeEvent<HTMLInputElement>) => void |
| Children | React.ReactNode for anything renderable, React.ReactElement for a single element |
| Ref | React.Ref<HTMLInputElement> for forwarded refs |
| Discriminated unions | type 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.
| Question | Recommendation |
|---|---|
| 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.
| Symptom | Tool | When to Apply |
|---|---|---|
| Child re-renders when parent state changes | React.memo() | Only if the child is expensive to render and props haven't changed |
| Expensive computation on every render | useMemo | Only if the computation takes >1ms and inputs rarely change |
| Callback causes child to re-render | useCallback | Only when passing callbacks to memoized children |
| Large list rendering | Virtualization (react-window, tanstack-virtual) | Lists with 100+ items visible in the DOM |
| Heavy component on initial load | React.lazy() + Suspense, or next/dynamic | Modals, charts, editors -- anything not visible on first paint |
| Layout shift from images | next/image with width/height or fill | Every 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 Type | Testing 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 validation | Test the happy path and each validation rule -- use Gherkin scenarios as specs |
| Data fetching | Mock the API, test loading/error/success states |
| Custom hook | Test 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.
| Element | Minimum Requirements |
|---|---|
| Buttons | Use <button>, not <div onClick>. Add aria-label if icon-only. |
| Inputs | Associate with <label> via htmlFor. Add aria-describedby for error messages. |
| Modals / dialogs | Trap focus, return focus on close. Use role="dialog" and aria-modal="true". |
| Images | alt text always. Decorative images get alt="". |
| Navigation | Use <nav>, <main>, <aside> landmarks. Ensure keyboard navigation works. |
| Dynamic content | Use aria-live regions for content that updates without page reload. |
| Color contrast | Minimum 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 Type | Location | Example |
|---|---|---|
| Page component | app/[route]/page.tsx | app/dashboard/page.tsx |
| Layout | app/[route]/layout.tsx | app/dashboard/layout.tsx |
| Shared UI component | components/[name].tsx | components/user-card.tsx |
| Feature-specific component | app/[feature]/components/[name].tsx | app/dashboard/components/stats-chart.tsx |
| Custom hook | hooks/[name].ts or co-located with its component | hooks/use-toggle.ts |
| Types | Co-locate in the same file, or types/[domain].ts for shared types | types/user.ts |
| Server Actions | app/[feature]/actions.ts | app/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
Related
- React Basics -- 15 component examples covering each concept in this workflow
- React & Next.js Architecture Decisions -- ranked choices for common architecture decisions
- Form Decision Checklist -- 20 questions to ask before building any form
- Best Practices -- React Fundamentals best practices summary
- Custom Hooks Guide -- when and how to extract custom hooks