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
Loading your progress

State Management Patterns

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

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

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

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

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

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

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

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

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

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

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