React logoReactv19ADVANCED

React Design Patterns

Advanced React patterns for state management, concurrent rendering, React 19 Actions, and Server Components

10 min read
reactpatternsreact-19concurrentserver-componentsactionsstate-managementreduceroptimistic-ui

Sign in to mark items as known and track your progress.

Sign in

State Management Patterns

Patterns for managing complex state with reducers, context, and custom hooks

useReducer Pattern

Manage complex state transitions with a reducer function and dispatched actions

jsx
function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'decrement': return { count: state.count - 1 };
    case 'reset':     return { count: 0 };
    default: throw Error('Unknown action: ' + action.type);
  }
}

const [state, dispatch] = useReducer(reducer, { count: 0 });
dispatch({ type: 'increment' });
💡 useReducer centralizes state logic - pure functions are easy to test in isolation
⚡ Action types should describe WHAT happened (user_added) not HOW to update (setUser)
📌 Pass an init function as the third arg for lazy initialization of expensive state
🟢 Use useReducer when state has 3+ sub-values or complex transitions - useState otherwise
useReducerstatereducer

Reducer + Context Pattern

Combine useReducer with Context to share complex state across the component tree (Redux-lite)

jsx
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);

function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(reducer, []);
  return (
    <TasksContext value={tasks}>
      <TasksDispatchContext value={dispatch}>
        {children}
      </TasksDispatchContext>
    </TasksContext>
  );
}
💡 Split state and dispatch into TWO contexts so dispatchers do not re-render on state changes
⚡ Wrap useContext in custom hooks (useTasks, useTasksDispatch) for ergonomics and type safety
📌 This is the standard "Redux without Redux" pattern - perfect for app-wide state without libraries
🟢 dispatch is stable across renders, so components that only dispatch never re-render unnecessarily
reducercontextstate-management

Custom Hooks Composition

Build complex behavior by composing smaller, focused custom hooks

jsx
function useDebouncedSearch(query, delay = 300) {
  const debounced = useDebounce(query, delay);
  const { data, loading } = useFetch(`/api/search?q=${debounced}`);
  return { results: data, loading };
}
💡 Custom hooks compose freely - useDebouncedSearch is just useDebounce + useFetch
⚡ Each hook should do ONE thing well - composition handles the complex cases
📌 Return objects when destructuring is more readable; arrays for tuple-like APIs
🟢 Always use the cancelled flag pattern in async effects to avoid memory leaks
custom-hookscomposition

Concurrent Rendering

Use React concurrent features to keep the UI responsive during expensive updates

useTransition

Mark state updates as non-urgent transitions to keep the UI responsive

jsx
const [isPending, startTransition] = useTransition();

function handleClick() {
  startTransition(() => {
    setTab('posts'); // non-urgent update
  });
}

return (
  <div>
    {isPending && <Spinner />}
    <Content tab={tab} />
  </div>
);
💡 useTransition tells React "this update is not urgent - keep the UI responsive"
⚡ Use it for tab switches, search filtering, and any heavy re-render triggered by user input
📌 React 19 supports async functions inside startTransition - perfect for form submissions
🟢 isPending lets you show subtle loading hints without blocking the click feedback
useTransitionconcurrentpending

useDeferredValue

Defer rendering of expensive UI based on a value while keeping inputs responsive

jsx
function SearchPage() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <ResultsList query={deferredQuery} className={isStale ? 'stale' : ''} />
    </>
  );
}
💡 useDeferredValue defers a VALUE; useTransition defers an UPDATE - pick based on what you control
⚡ Always memoize the expensive child - otherwise React still re-renders it on every keystroke
📌 Compare value !== deferredValue to detect stale state and show a visual hint
🟢 React renders twice: once with the old value, then again in the background with the new one
useDeferredValueconcurrentperformance

React 19 Actions & Forms

New form handling pattern with built-in pending state, errors, and optimistic updates

useActionState

Manage form state, errors, and pending status with a single hook

jsx
const [state, formAction, isPending] = useActionState(
  async (prevState, formData) => {
    const result = await updateUser(formData.get('name'));
    return result.error ? result : { success: true };
  },
  null
);

return (
  <form action={formAction}>
    <input name="name" />
    <button disabled={isPending}>Save</button>
    {state?.error && <p>{state.error}</p>}
  </form>
);
💡 useActionState replaces the old useState dance for form loading + error handling
⚡ The action receives previousState and formData - return any state shape you need
📌 Forms work without JavaScript when paired with server actions - true progressive enhancement
🟢 Use defaultValue (not value) so the form is uncontrolled and works with form data
useActionStateformsreact-19

useOptimistic

Show instant UI feedback while async operations are still in flight

jsx
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
  todos,
  (state, newTodo) => [...state, { ...newTodo, pending: true }]
);

async function handleAdd(text) {
  addOptimisticTodo({ id: 'temp', text });
  await api.addTodo({ text });
}
💡 useOptimistic shows instant feedback then reverts automatically if the action fails
⚡ Must be called inside startTransition or a form action - React enforces this
📌 Add a pending flag in the optimistic state to visually distinguish unsaved items
🟢 No manual rollback needed - React reconciles when the real state updates
useOptimisticoptimistic-uireact-19

useFormStatus & form Actions

Read pending state from a parent form without prop drilling

jsx
function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? 'Saving...' : 'Save'}</button>;
}

function MyForm() {
  return (
    <form action={async (formData) => { await save(formData); }}>
      <input name="title" />
      <SubmitButton />
    </form>
  );
}
💡 useFormStatus reads parent form state - perfect for SubmitButton without prop drilling
⚡ Form actions auto-reset the form after success and provide automatic pending state
📌 useFormStatus only works in CHILD components - wrap your button in its own component
🟢 Combine with useActionState for full form state management without useState
useFormStatusformsreact-19

Server Components

Render components on the server, fetch data directly, and mix with client components

Server Components Basics

Async components that run on the server with direct data access

jsx
// app/posts/page.tsx - Server Component (default)
async function PostsPage() {
  const posts = await db.post.findMany();

  return (
    <ul>
      {posts.map(p => <li key={p.id}>{p.title}</li>)}
    </ul>
  );
}
💡 Server Components are async, run only on the server, and never ship JS to the browser
⚡ Pass Server Components as children to Client Components - best of both worlds
📌 Props from Server → Client must be serializable: no functions, classes, or Dates
🟢 Default to Server Components and opt into Client Components only when you need interactivity
rscserver-componentsreact-19

Server Actions ('use server')

Call server functions directly from client components and forms

jsx
// actions.ts
'use server';
export async function createPost(formData) {
  await db.post.create({ data: { title: formData.get('title') } });
  revalidatePath('/posts');
}

// PostForm.tsx (Client)
import { createPost } from './actions';

function PostForm() {
  return <form action={createPost}><input name="title" /></form>;
}
💡 Server actions look like regular function calls but execute on the server with direct DB access
⚡ Use revalidatePath/revalidateTag to refresh the cached UI after a mutation
📌 Server actions in form action props give progressive enhancement - works without JavaScript
🟢 Always validate inputs in server actions - they are public endpoints accessible to anyone
server-actionsuse-servermutations

Component API Patterns

Patterns for designing flexible and reusable component APIs

Polymorphic Components (as prop)

One component that can render as different elements based on an "as" prop

jsx
function Box({ as: Component = 'div', ...props }) {
  return <Component {...props} />;
}

<Box>div by default</Box>
<Box as="section">renders a section</Box>
<Box as="a" href="/home">renders an anchor</Box>
<Box as={Link} to="/home">renders a Link</Box>
💡 The "as" prop pattern keeps semantics flexible while sharing styling and behavior
⚡ Default the "as" prop with a destructure default (as: Component = "div")
📌 Type-safe polymorphism in TypeScript needs ElementType + ComponentPropsWithoutRef
🟢 Used by every major React UI library - Radix, Chakra, Mantine, shadcn/ui
polymorphicas-propapi

Controlled vs Uncontrolled

Two ways to manage component state - let parent control, or manage internally

jsx
// Controlled - parent owns the state
<Input value={value} onChange={setValue} />

// Uncontrolled - component owns the state
<Input defaultValue="hello" ref={inputRef} />
💡 Controlled = parent owns state; Uncontrolled = DOM/component owns state
⚡ Building a component to support both is a common library pattern (Radix, MUI)
📌 React warns if you switch between controlled/uncontrolled - pick one and stick with it
🟢 react-hook-form leverages uncontrolled inputs for fewer re-renders on large forms
controlleduncontrolledforms

Provider Pattern with Custom Hooks

Wrap context with a Provider component and a custom hook for safe consumption

jsx
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  return <ThemeContext value={{ theme, setTheme }}>{children}</ThemeContext>;
}

function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme must be inside ThemeProvider');
  return ctx;
}
💡 The Provider + custom hook combo is the standard way to expose Context safely
⚡ Throwing in the custom hook gives clear errors when the Provider is missing
📌 Encapsulating state inside the Provider lets you refactor to Zustand/Jotai later without breaking consumers
🟢 Split state and dispatch into separate contexts to prevent unnecessary re-renders
providercontextcustom-hooks