JavaScript
React Advanced Patterns Production Guide
Master advanced React patterns including hooks, context, render props, HOCs, custom hooks, performance optimisation, and production deployment with TypeScript.
What you'll learn
- Implement compound component patterns
- Build type-safe custom hooks
- Use render props and HOCs correctly
- Manage complex state with context + useReducer
- Implement optimistic UI updates
- Write accessible component APIs
- Test advanced patterns with Vitest and Testing Library
Compound Components
Compound components share implicit state via context, giving consumers a flexible, ergonomic API without prop drilling.
React TSX
1import { createContext, useContext, useState } from 'react';
2
3interface TabsCtx { active: string; setActive: (v: string) => void }
4const TabsContext = createContext<TabsCtx | null>(null);
5
6function useTabs() {
7 const ctx = useContext(TabsContext);
8 if (!ctx) throw new Error('useTabs must be used inside <Tabs>');
9 return ctx;
10}
11
12function Tabs({ defaultValue, children }: { defaultValue: string; children: React.ReactNode }) {
13 const [active, setActive] = useState(defaultValue);
14 return <TabsContext.Provider value={{ active, setActive }}>{children}</TabsContext.Provider>;
15}
16
17function TabsList({ children }: { children: React.ReactNode }) {
18 return <div role="tablist" className="flex gap-2">{children}</div>;
19}
20
21function TabsTrigger({ value, children }: { value: string; children: React.ReactNode }) {
22 const { active, setActive } = useTabs();
23 return (
24 <button
25 role="tab"
26 aria-selected={active === value}
27 onClick={() => setActive(value)}
28 className={active === value ? 'font-bold border-b-2 border-primary' : 'text-muted-foreground'}
29 >
30 {children}
31 </button>
32 );
33}
34
35function TabsContent({ value, children }: { value: string; children: React.ReactNode }) {
36 const { active } = useTabs();
37 if (active !== value) return null;
38 return <div role="tabpanel">{children}</div>;
39}
40
41// Attach sub-components
42Tabs.List = TabsList;
43Tabs.Trigger = TabsTrigger;
44Tabs.Content = TabsContent;
45
46// Usage
47<Tabs defaultValue="overview">
48 <Tabs.List>
49 <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
50 <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
51 </Tabs.List>
52 <Tabs.Content value="overview"><p>Overview content</p></Tabs.Content>
53 <Tabs.Content value="settings"><p>Settings content</p></Tabs.Content>
54</Tabs>Custom Hooks
TypeScript
1// useDebounce — type-safe, generic
2import { useState, useEffect } from 'react';
3
4export function useDebounce<T>(value: T, delay: number): T {
5 const [debouncedValue, setDebouncedValue] = useState<T>(value);
6
7 useEffect(() => {
8 const handler = setTimeout(() => setDebouncedValue(value), delay);
9 return () => clearTimeout(handler);
10 }, [value, delay]);
11
12 return debouncedValue;
13}
14
15// useLocalStorage — SSR-safe
16export function useLocalStorage<T>(key: string, initialValue: T) {
17 const [storedValue, setStoredValue] = useState<T>(() => {
18 if (typeof window === 'undefined') return initialValue;
19 try {
20 const item = window.localStorage.getItem(key);
21 return item ? (JSON.parse(item) as T) : initialValue;
22 } catch {
23 return initialValue;
24 }
25 });
26
27 const setValue = (value: T | ((val: T) => T)) => {
28 const valueToStore = value instanceof Function ? value(storedValue) : value;
29 setStoredValue(valueToStore);
30 if (typeof window !== 'undefined') {
31 window.localStorage.setItem(key, JSON.stringify(valueToStore));
32 }
33 };
34
35 return [storedValue, setValue] as const;
36}Render Props & HOCs
React TSX
1// Prefer custom hooks over HOCs for logic reuse.
2// HOCs remain useful for cross-cutting concerns like auth guards.
3
4function withAuth<P extends object>(WrappedComponent: React.ComponentType<P>) {
5 return function AuthGuard(props: P) {
6 const { isAuthenticated, isLoading } = useAuth();
7 if (isLoading) return <Spinner />;
8 if (!isAuthenticated) return <Navigate to="/login" replace />;
9 return <WrappedComponent {...props} />;
10 };
11}
12
13const ProtectedDashboard = withAuth(Dashboard);Advanced State Management
TypeScript
1// Context + useReducer for complex domain state
2type Action =
3 | { type: 'INCREMENT'; by: number }
4 | { type: 'RESET' };
5
6function reducer(state: number, action: Action): number {
7 switch (action.type) {
8 case 'INCREMENT': return state + action.by;
9 case 'RESET': return 0;
10 }
11}
12
13// For global client state, prefer Zustand:
14import { create } from 'zustand';
15
16interface CartStore {
17 items: CartItem[];
18 addItem: (item: CartItem) => void;
19 removeItem: (id: string) => void;
20}
21
22const useCartStore = create<CartStore>((set) => ({
23 items: [],
24 addItem: (item) => set((s) => ({ items: [...s.items, item] })),
25 removeItem: (id) => set((s) => ({ items: s.items.filter((i) => i.id !== id) })),
26}));Optimistic UI
TypeScript
1import { useMutation, useQueryClient } from '@tanstack/react-query';
2
3function useLikeMutation() {
4 const queryClient = useQueryClient();
5
6 return useMutation({
7 mutationFn: (postId: string) =>
8 fetch(`${process.env['NEXT_PUBLIC_API_URL']}/posts/${postId}/like`, {
9 method: 'POST',
10 credentials: 'include',
11 }),
12
13 onMutate: async (postId) => {
14 await queryClient.cancelQueries({ queryKey: ['post', postId] });
15 const snapshot = queryClient.getQueryData(['post', postId]);
16 queryClient.setQueryData(['post', postId], (old: Post) => ({
17 ...old,
18 likes: old.likes + 1,
19 isLiked: true,
20 }));
21 return { snapshot };
22 },
23
24 onError: (_err, postId, ctx) => {
25 // Roll back on failure
26 queryClient.setQueryData(['post', postId], ctx?.snapshot);
27 },
28 });
29}Testing Patterns
React TSX
1import { render, screen, userEvent } from '@testing-library/react';
2import { describe, it, expect } from 'vitest';
3import { Tabs } from './Tabs';
4
5describe('Tabs compound component', () => {
6 it('renders the default tab content', () => {
7 render(
8 <Tabs defaultValue="a">
9 <Tabs.List>
10 <Tabs.Trigger value="a">Tab A</Tabs.Trigger>
11 <Tabs.Trigger value="b">Tab B</Tabs.Trigger>
12 </Tabs.List>
13 <Tabs.Content value="a">Content A</Tabs.Content>
14 <Tabs.Content value="b">Content B</Tabs.Content>
15 </Tabs>
16 );
17 expect(screen.getByText('Content A')).toBeInTheDocument();
18 expect(screen.queryByText('Content B')).not.toBeInTheDocument();
19 });
20
21 it('switches tab on click', async () => {
22 const user = userEvent.setup();
23 render(/* same tree */);
24 await user.click(screen.getByRole('tab', { name: 'Tab B' }));
25 expect(screen.getByText('Content B')).toBeInTheDocument();
26 });
27});