Skip to content

Common React Patterns

Fresh

Component Patterns

Container / Presentational

Separate data-fetching logic from display logic:

jsx
// Container (handles data)
function UserListContainer() {
  const [users, setUsers] = useState([]);
  useEffect(() => {
    fetch('/api/users').then(r => r.json()).then(setUsers);
  }, []);
  return <UserList users={users} />;
}

// Presentational (handles display)
function UserList({ users }) {
  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

Compound Components

Components that work together to form a complete UI:

jsx
function Select({ children, value, onChange }) {
  return (
    <SelectContext.Provider value={{ value, onChange }}>
      <div className="select">{children}</div>
    </SelectContext.Provider>
  );
}

function Option({ value, children }) {
  const { value: selected, onChange } = useContext(SelectContext);
  return (
    <div
      className={value === selected ? 'selected' : ''}
      onClick={() => onChange(value)}
    >
      {children}
    </div>
  );
}

// Usage
<Select value={color} onChange={setColor}>
  <Option value="red">Red</Option>
  <Option value="blue">Blue</Option>
</Select>

Render Props

Pass a function as a prop to share logic:

jsx
function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);

  return render(position);
}

// Usage
<MouseTracker render={({ x, y }) => <p>Mouse: {x}, {y}</p>} />

TIP

Custom Hooks have largely replaced render props for sharing logic. Prefer hooks when possible.

Higher-Order Components (HOC)

A function that takes a component and returns a new component:

jsx
function withAuth(WrappedComponent) {
  return function AuthenticatedComponent(props) {
    const user = useAuth();
    if (!user) return <LoginPage />;
    return <WrappedComponent {...props} user={user} />;
  };
}

const ProtectedDashboard = withAuth(Dashboard);

TIP

Like render props, HOCs have been largely superseded by custom Hooks. Prefer hooks for new code.

State Patterns

Controlled vs Uncontrolled Components

jsx
// Controlled: React manages the value
function ControlledInput() {
  const [value, setValue] = useState('');
  return <input value={value} onChange={e => setValue(e.target.value)} />;
}

// Uncontrolled: DOM manages the value
function UncontrolledInput() {
  const inputRef = useRef(null);
  function handleSubmit() {
    console.log(inputRef.current.value);
  }
  return <input defaultValue="" ref={inputRef} />;
}

State Reducer Pattern

Give consumers control over state logic:

jsx
function useToggle({ reducer = toggleReducer } = {}) {
  const [state, dispatch] = useReducer(reducer, { on: false });
  const toggle = () => dispatch({ type: 'toggle' });
  return { on: state.on, toggle };
}

function toggleReducer(state, action) {
  switch (action.type) {
    case 'toggle':
      return { on: !state.on };
    default:
      return state;
  }
}

Data Fetching Patterns

useEffect + useState

jsx
function useData(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);

    fetch(url)
      .then(res => res.json())
      .then(data => {
        if (!cancelled) {
          setData(data);
          setLoading(false);
        }
      })
      .catch(err => {
        if (!cancelled) {
          setError(err);
          setLoading(false);
        }
      });

    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}

Avoid Fetching in useEffect for Production

For production apps, use a framework with built-in data fetching (Next.js, Remix) or a client-side caching library (React Query, SWR). Raw useEffect fetching misses caching, race conditions, and server rendering.

Performance Patterns

Debouncing State Updates

jsx
function SearchInput({ onSearch }) {
  const [query, setQuery] = useState('');
  const timeoutRef = useRef(null);

  function handleChange(e) {
    const value = e.target.value;
    setQuery(value);
    clearTimeout(timeoutRef.current);
    timeoutRef.current = setTimeout(() => onSearch(value), 300);
  }

  return <input value={query} onChange={handleChange} />;
}

Virtualized Lists

For long lists, only render visible items:

jsx
// Use react-window or @tanstack/react-virtual
import { FixedSizeList } from 'react-window';

function VirtualList({ items }) {
  return (
    <FixedSizeList
      height={400}
      itemCount={items.length}
      itemSize={35}
      width={300}
    >
      {({ index, style }) => (
        <div style={style}>{items[index]}</div>
      )}
    </FixedSizeList>
  );
}

Lazy Loading Components

jsx
import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <HeavyComponent />
    </Suspense>
  );
}

Anti-Patterns to Avoid

Anti-PatternProblemSolution
Mutating state directlyReact won't detect changesCreate new objects/arrays
Derived state in useStateState gets out of syncCompute during render
useEffect for eventsRuns on every render, not just eventsUse event handlers
Props in initial stateInitial state ignores prop updatesUse props directly or add a key
Index as keyBugs with reordering/filteringUse stable unique IDs
Deeply nested stateHard to update immutablyFlatten/normalize state