← All stacks
RE

React

16 sections · 105 entries

Components

Function Component

syntax
function ComponentName(props) {
  return <JSX />;
}
// or arrow function
const ComponentName = (props) => <JSX />;
example
function Greeting({ name }) {
  return <h1>Welcome, {name}!</h1>;
}

// Usage
<Greeting name="Amara" />
output
Renders: <h1>Welcome, Amara!</h1>

Note Component names must start with a capital letter. React treats lowercase tags as native HTML elements.

Props

syntax
function Component({ propA, propB }) { ... }
// or
function Component(props) {
  const { propA, propB } = props;
}
example
function UserCard({ username, role, isActive }) {
  return (
    <div>
      <strong>{username}</strong>
      <span>{role}</span>
      {isActive && <span>Online</span>}
    </div>
  );
}
output
Renders the user card with username, role, and optional online badge.

Note Props are read-only. Never mutate a prop directly inside a component. Destructuring in the parameter list is the most common pattern.

Children Prop

syntax
function Wrapper({ children }) {
  return <div className="wrapper">{children}</div>;
}
example
function Card({ children, title }) {
  return (
    <section className="card">
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
    </section>
  );
}

// Usage
<Card title="Profile">
  <p>This content is passed as children.</p>
</Card>
output
Renders a card wrapper around the paragraph.

Note children can be any renderable content: text, elements, arrays, or even functions (for render props).

Default Prop Values

syntax
function Component({ propA = defaultValue }) { ... }
example
function Badge({ label = "Member", color = "gray" }) {
  return (
    <span style={{ backgroundColor: color }}>
      {label}
    </span>
  );
}

// Uses defaults
<Badge />
// Overrides
<Badge label="Admin" color="crimson" />
output
First renders "Member" with gray background. Second renders "Admin" with crimson.

Note Use default parameter values in the destructuring rather than the legacy defaultProps static property, which is deprecated in React 19.

Component Composition

syntax
function Parent() {
  return (
    <Layout>
      <Header />
      <Content />
      <Footer />
    </Layout>
  );
}
example
function PageLayout({ sidebar, content }) {
  return (
    <div className="layout">
      <aside>{sidebar}</aside>
      <main>{content}</main>
    </div>
  );
}

function Dashboard() {
  return (
    <PageLayout
      sidebar={<NavMenu />}
      content={<StatsPanel />}
    />
  );
}

Note Composition via props is preferred over inheritance. Passing components as props gives you flexible "slot" patterns without tightly coupling parent and child.

Conditional Rendering

syntax
{condition && <Component />}
{condition ? <A /> : <B />}
if (condition) return <A />; return <B />;
example
function Notification({ message, type }) {
  if (!message) return null;

  return (
    <div className={`alert alert-${type}`}>
      {type === "error" ? "Error: " : "Info: "}
      {message}
    </div>
  );
}
output
Returns null when no message. Otherwise renders an alert with type prefix.

Note Returning null renders nothing. Watch out with &&: if the left side is 0, React renders "0" because it is a valid JSX child. Use Boolean(count) && <Component /> or count > 0 && <Component /> instead.

Lists & Keys

syntax
{items.map(item => (
  <Component key={item.uniqueId} {...item} />
))}
example
function TaskList({ tasks }) {
  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          <span>{task.title}</span>
          {task.done && <span> (completed)</span>}
        </li>
      ))}
    </ul>
  );
}

Note Keys must be stable, unique among siblings, and derived from data (like a database ID). Never use array index as key if the list order can change -- it causes subtle rendering bugs and broken state.

Fragments

syntax
<React.Fragment>...</React.Fragment>
// Short syntax
<>...</>
example
function UserInfo({ name, email }) {
  return (
    <>
      <dt>{name}</dt>
      <dd>{email}</dd>
    </>
  );
}

// With key (long syntax required)
function GlossaryList({ items }) {
  return (
    <dl>
      {items.map(item => (
        <React.Fragment key={item.id}>
          <dt>{item.term}</dt>
          <dd>{item.definition}</dd>
        </React.Fragment>
      ))}
    </dl>
  );
}

Note Fragments let you group elements without adding extra DOM nodes. Use the long syntax <React.Fragment key={...}> when mapping a list, since the short syntax does not support the key attribute.

JSX

JSX Syntax Rules

syntax
// Must return single root element
return (
  <div>
    <Child />
  </div>
);
// Self-closing tags required for void elements
<img src={url} alt="desc" />
example
function ProductCard({ product }) {
  return (
    <article className="product-card">
      <img src={product.imageUrl} alt={product.name} />
      <h3>{product.name}</h3>
      <p>{product.description}</p>
      <strong>${product.price.toFixed(2)}</strong>
    </article>
  );
}

Note JSX is syntactic sugar for function calls. Every tag must be closed. Use className instead of class, htmlFor instead of for. Attribute names follow camelCase convention.

Embedding Expressions

syntax
{expression}
// Any valid JavaScript expression inside curly braces
example
function PriceDisplay({ price, discount }) {
  const finalPrice = price * (1 - discount);
  const savings = price - finalPrice;

  return (
    <div>
      <p>Original: ${price.toFixed(2)}</p>
      <p>You save: ${savings.toFixed(2)}</p>
      <p className="total">Total: ${finalPrice.toFixed(2)}</p>
      <small>Applied {(discount * 100).toFixed(0)}% off</small>
    </div>
  );
}

Note You can embed any JavaScript expression inside curly braces: variables, function calls, math, ternaries. Statements (if, for, while) cannot appear inside JSX directly -- use them before the return.

Conditional Rendering Patterns

syntax
// AND pattern
{isLoggedIn && <Dashboard />}
// Ternary
{isLoggedIn ? <Dashboard /> : <LoginForm />}
// IIFE for complex logic
{(() => { if (x) return <A />; return <B />; })()}
example
function StatusBadge({ status }) {
  return (
    <span>
      {status === "active" && <span className="dot green" />}
      {status === "inactive" ? (
        <span className="label">Away</span>
      ) : (
        <span className="label">{status}</span>
      )}
    </span>
  );
}

Note For more than two branches, extract the logic into a variable or helper function above the return statement rather than nesting ternaries. Readable code wins over clever one-liners.

Spread Attributes

syntax
<Component {...propsObject} />
example
function FormField({ label, ...inputProps }) {
  return (
    <div className="field">
      <label>{label}</label>
      <input {...inputProps} />
    </div>
  );
}

// Usage: all standard input attributes are forwarded
<FormField
  label="Email"
  type="email"
  placeholder="you@example.com"
  required
/>

Note Spread is great for forwarding props. Be careful: it can pass unintended attributes to DOM elements. Destructure known props first, then spread the rest.

JSX vs createElement

syntax
// JSX (compiled to createElement)
<Button color="blue">Save</Button>

// Equivalent createElement call
React.createElement(Button, { color: "blue" }, "Save")
example
// This JSX:
const element = (
  <div className="container">
    <h1>Title</h1>
    <p>Paragraph</p>
  </div>
);

// Compiles to:
const element = React.createElement(
  "div",
  { className: "container" },
  React.createElement("h1", null, "Title"),
  React.createElement("p", null, "Paragraph")
);

Note You almost never need to call createElement directly. JSX is syntactically cleaner and the standard in the ecosystem. The JSX transform in modern setups does not require importing React at the top of every file.

Inline Style Objects

syntax
<div style={{ property: value }} />
example
function ProgressBar({ percent }) {
  return (
    <div
      style={{
        width: `${percent}%`,
        height: "8px",
        backgroundColor: percent > 70 ? "#4caf50" : "#ff9800",
        borderRadius: "4px",
        transition: "width 0.3s ease",
      }}
    />
  );
}

Note Style properties use camelCase (backgroundColor, not background-color). Values that need units must include them as strings ("8px"), except for unitless properties like opacity and zIndex which accept numbers.

useState

Basic Usage

syntax
const [state, setState] = useState(initialValue);
example
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}
output
Displays count starting at 0. Clicking increment adds 1 each time.

Note setState triggers a re-render. The state value remains the same within the current render cycle even after calling setState -- the new value appears on the next render.

Updater Function

syntax
setState(prevState => newState);
example
function StepCounter() {
  const [count, setCount] = useState(0);

  function incrementThree() {
    // Each updater receives the latest pending state
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
  }

  return (
    <button onClick={incrementThree}>
      Count: {count}
    </button>
  );
}
output
Each click adds 3 to the count, because each updater builds on the previous pending value.

Note Always use the updater form when the next state depends on the previous state. Writing setCount(count + 1) three times in a row would only add 1 because count is a stale snapshot from the current render.

Lazy Initialization

syntax
const [state, setState] = useState(() => computeInitialState());
example
function FilteredList() {
  // Expensive computation runs only on mount, not every render
  const [sortedItems, setSortedItems] = useState(() => {
    const saved = localStorage.getItem('cachedItems');
    return saved ? JSON.parse(saved) : [];
  });

  return <ItemList items={sortedItems} />;
}

Note Pass a function (not the result of calling it) to avoid running expensive initialization logic on every render. React only calls the initializer function during the first render and ignores it on subsequent renders.

Objects in State

syntax
setState(prev => ({ ...prev, key: newValue }));
example
function ProfileEditor() {
  const [profile, setProfile] = useState({
    firstName: '',
    lastName: '',
    bio: '',
  });

  function handleChange(field, value) {
    setProfile(prev => ({
      ...prev,
      [field]: value,
    }));
  }

  return (
    <>
      <input
        value={profile.firstName}
        onChange={e => handleChange('firstName', e.target.value)}
        placeholder="First name"
      />
      <input
        value={profile.lastName}
        onChange={e => handleChange('lastName', e.target.value)}
        placeholder="Last name"
      />
    </>
  );
}

Note Always spread the previous object to create a new copy. Directly mutating state (profile.firstName = 'X') will NOT trigger a re-render because React compares by reference.

Arrays in State

syntax
// Add
setItems(prev => [...prev, newItem]);
// Remove
setItems(prev => prev.filter(i => i.id !== id));
// Update one
setItems(prev => prev.map(i => i.id === id ? { ...i, done: true } : i));
example
function TodoList() {
  const [todos, setTodos] = useState([]);
  const [text, setText] = useState('');

  function addTodo() {
    if (!text.trim()) return;
    setTodos(prev => [
      ...prev,
      { id: crypto.randomUUID(), text, done: false },
    ]);
    setText('');
  }

  function removeTodo(id) {
    setTodos(prev => prev.filter(t => t.id !== id));
  }

  function toggleTodo(id) {
    setTodos(prev =>
      prev.map(t => t.id === id ? { ...t, done: !t.done } : t)
    );
  }

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={addTodo}>Add</button>
      <ul>
        {todos.map(t => (
          <li key={t.id}>
            <span
              style={{ textDecoration: t.done ? 'line-through' : 'none' }}
              onClick={() => toggleTodo(t.id)}
            >
              {t.text}
            </span>
            <button onClick={() => removeTodo(t.id)}>Remove</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Note Use filter to remove, map to update, and spread to add. Never use push, splice, or direct index assignment on state arrays -- those mutate the existing array instead of creating a new one.

Multiple State Variables

syntax
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [isActive, setIsActive] = useState(false);
example
function RegistrationForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [acceptTerms, setAcceptTerms] = useState(false);

  const isValid = email.includes('@') && password.length >= 8 && acceptTerms;

  return (
    <form>
      <input
        type="email"
        value={email}
        onChange={e => setEmail(e.target.value)}
      />
      <input
        type="password"
        value={password}
        onChange={e => setPassword(e.target.value)}
      />
      <label>
        <input
          type="checkbox"
          checked={acceptTerms}
          onChange={e => setAcceptTerms(e.target.checked)}
        />
        I accept the terms
      </label>
      <button disabled={!isValid}>Register</button>
    </form>
  );
}

Note Separate state variables are preferred when values change independently. Combine into an object only when values logically belong together and always change at the same time.

useEffect

Basic Usage

syntax
useEffect(() => {
  // side effect
  return () => { /* cleanup */ };
}, [dependencies]);
example
import { useState, useEffect } from 'react';

function DocumentTitle({ title }) {
  useEffect(() => {
    document.title = title;
  }, [title]);

  return <h1>{title}</h1>;
}

Note The effect runs after the component renders. The dependency array controls when it re-runs. Omit the array: runs after every render. Empty array []: runs once on mount. With values: runs when any dependency changes.

Dependency Array

syntax
useEffect(effectFn, [dep1, dep2]);
// Empty array: mount only
useEffect(effectFn, []);
// No array: every render
useEffect(effectFn);
example
function SearchResults({ query, page }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    // Runs whenever query or page changes
    let cancelled = false;

    async function search() {
      const data = await fetchResults(query, page);
      if (!cancelled) setResults(data);
    }
    search();

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

  return <ResultsList items={results} />;
}

Note React uses Object.is to compare dependencies. Objects and arrays create new references each render, so they will always trigger the effect. Extract primitive values or memoize objects before listing them as dependencies.

Cleanup Function

syntax
useEffect(() => {
  // setup
  return () => {
    // cleanup runs before next effect and on unmount
  };
}, [deps]);
example
function ChatConnection({ roomId }) {
  useEffect(() => {
    const connection = connectToRoom(roomId);
    connection.open();

    return () => {
      connection.close();
    };
  }, [roomId]);

  return <ChatWindow />;
}

Note The cleanup function runs before the effect re-executes (when dependencies change) and when the component unmounts. This prevents memory leaks from subscriptions, timers, and open connections.

Fetching Data

syntax
useEffect(() => {
  let ignore = false;
  async function load() {
    const data = await fetchData();
    if (!ignore) setState(data);
  }
  load();
  return () => { ignore = true; };
}, [deps]);
example
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

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

    async function loadUser() {
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();
      if (!ignore) {
        setUser(data);
        setLoading(false);
      }
    }
    loadUser();

    return () => { ignore = true; };
  }, [userId]);

  if (loading) return <p>Loading...</p>;
  return <div>{user.name}</div>;
}

Note The ignore flag prevents setting state on an unmounted component or after a stale request. In React 19, consider using the use() hook with Suspense for data fetching instead of this pattern.

Event Listeners

syntax
useEffect(() => {
  const handler = (e) => { ... };
  window.addEventListener('event', handler);
  return () => window.removeEventListener('event', handler);
}, []);
example
function WindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    function handleResize() {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    }

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return <p>{size.width} x {size.height}</p>;
}

Note Always remove event listeners in the cleanup to avoid duplicate handlers and memory leaks. The handler reference must be the same function for addEventListener and removeEventListener.

Timers

syntax
useEffect(() => {
  const id = setInterval(() => { ... }, delay);
  return () => clearInterval(id);
}, [delay]);
example
function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const [running, setRunning] = useState(false);

  useEffect(() => {
    if (!running) return;

    const intervalId = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    return () => clearInterval(intervalId);
  }, [running]);

  return (
    <div>
      <p>{seconds}s</p>
      <button onClick={() => setRunning(r => !r)}>
        {running ? 'Pause' : 'Start'}
      </button>
      <button onClick={() => { setRunning(false); setSeconds(0); }}>
        Reset
      </button>
    </div>
  );
}

Note Always clear intervals and timeouts in cleanup. Use the updater form (prev => prev + 1) inside intervals to avoid stale closure issues with the state value.

When NOT to Use useEffect

syntax
// Derived state: compute during render
const fullName = firstName + ' ' + lastName;

// Event response: handle in the event handler
function handleClick() { doSomething(); }
example
// BAD: useEffect to sync derived data
const [firstName, setFirstName] = useState('Kai');
const [lastName, setLastName] = useState('Chen');
const [fullName, setFullName] = useState('');

useEffect(() => {
  setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);

// GOOD: compute directly during render
const fullName = firstName + ' ' + lastName;

// BAD: useEffect for event-driven logic
useEffect(() => {
  if (submitted) sendAnalytics();
}, [submitted]);

// GOOD: call directly in the event handler
function handleSubmit() {
  submitForm();
  sendAnalytics();
}

Note Overusing useEffect is one of the most common React mistakes. If you can calculate something from existing props or state, do it during render. If something should happen in response to a user action, put it in the event handler.

useRef

DOM Refs

syntax
const ref = useRef(null);
// Attach to element
<element ref={ref} />
// Access
ref.current
example
import { useRef } from 'react';

function AutoFocusInput() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
    inputRef.current.select();
  }

  return (
    <div>
      <input ref={inputRef} defaultValue="Edit me" />
      <button onClick={handleClick}>Focus & Select</button>
    </div>
  );
}

Note ref.current is null during the first render and gets assigned after the DOM node mounts. Never read or write ref.current during rendering -- only inside effects or event handlers.

Mutable Values (No Re-render)

syntax
const valueRef = useRef(initialValue);
valueRef.current = newValue; // does NOT trigger re-render
example
function ClickTracker() {
  const clickCount = useRef(0);

  function handleClick() {
    clickCount.current += 1;
    console.log(`Clicked ${clickCount.current} times`);
  }

  return <button onClick={handleClick}>Click me</button>;
}

// Common use: storing previous value
function usePrevious(value) {
  const prevRef = useRef(undefined);
  const previous = prevRef.current;
  prevRef.current = value;
  return previous;
}

Note useRef is like an instance variable for function components. Updating .current does not cause a re-render. Use it for values you need to persist across renders without triggering UI updates: timer IDs, previous values, flags.

Forwarding Refs (React 19)

syntax
// React 19: ref is a regular prop
function CustomInput({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

// Pre-React 19 (still works):
const CustomInput = forwardRef((props, ref) => {
  return <input ref={ref} {...props} />;
});
example
// React 19 style: ref is just a prop
function FancyInput({ ref, placeholder, className }) {
  return (
    <input
      ref={ref}
      placeholder={placeholder}
      className={`fancy-input ${className}`}
    />
  );
}

// Parent component
function SearchBar() {
  const inputRef = useRef(null);

  return (
    <div>
      <FancyInput ref={inputRef} placeholder="Search..." />
      <button onClick={() => inputRef.current.focus()}>Focus</button>
    </div>
  );
}

Note In React 19, ref is passed as a regular prop to function components. forwardRef is no longer necessary and is considered deprecated. Existing code using forwardRef still works but should be migrated over time.

Callback Refs

syntax
<element ref={(node) => {
  // node is the DOM element or null on unmount
}} />
example
function MeasuredBox() {
  const [height, setHeight] = useState(0);

  const measureRef = (node) => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  };

  return (
    <div>
      <div ref={measureRef} style={{ padding: '20px' }}>
        <p>This box has dynamic content.</p>
        <p>Its height is measured via a callback ref.</p>
      </div>
      <p>Measured height: {height}px</p>
    </div>
  );
}

Note Callback refs fire whenever the ref attaches or detaches. In React 19, callback refs can return a cleanup function that runs when the element is removed, similar to useEffect cleanup. Useful for measuring elements or integrating third-party DOM libraries.

useImperativeHandle

syntax
useImperativeHandle(ref, () => ({
  customMethod() { ... },
}), [deps]);
example
import { useRef, useImperativeHandle } from 'react';

function VideoPlayer({ ref, src }) {
  const videoRef = useRef(null);

  useImperativeHandle(ref, () => ({
    play() {
      videoRef.current.play();
    },
    pause() {
      videoRef.current.pause();
    },
    restart() {
      videoRef.current.currentTime = 0;
      videoRef.current.play();
    },
  }));

  return <video ref={videoRef} src={src} />;
}

// Parent controls the player
function MediaPage() {
  const playerRef = useRef(null);

  return (
    <div>
      <VideoPlayer ref={playerRef} src="/video.mp4" />
      <button onClick={() => playerRef.current.play()}>Play</button>
      <button onClick={() => playerRef.current.restart()}>Restart</button>
    </div>
  );
}

Note useImperativeHandle restricts what the parent can access through the ref. Expose only the methods the parent needs instead of the entire DOM node. Combine with ref-as-prop in React 19 (no forwardRef needed).

Storing Timer IDs

syntax
const timerRef = useRef(null);
// Start
timerRef.current = setTimeout(fn, ms);
// Clear
clearTimeout(timerRef.current);
example
function DebouncedSearch({ onSearch }) {
  const [query, setQuery] = useState('');
  const timerRef = useRef(null);

  function handleChange(e) {
    const value = e.target.value;
    setQuery(value);

    clearTimeout(timerRef.current);
    timerRef.current = setTimeout(() => {
      onSearch(value);
    }, 400);
  }

  useEffect(() => {
    return () => clearTimeout(timerRef.current);
  }, []);

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

Note Store timer IDs in a ref so they survive re-renders without causing them. Always clear the timer on unmount via useEffect cleanup to prevent firing after the component is gone.

useContext

Creating Context

syntax
import { createContext } from 'react';
const MyContext = createContext(defaultValue);
example
// theme-context.js
import { createContext } from 'react';

export const ThemeContext = createContext({
  mode: 'light',
  toggleTheme: () => {},
});

Note The default value is used only when a component reads the context but there is no matching Provider above it in the tree. Pass a realistic shape as the default so TypeScript inference and fallback behavior work correctly.

Providing Context

syntax
<MyContext.Provider value={contextValue}>
  {children}
</MyContext.Provider>
example
import { useState } from 'react';
import { ThemeContext } from './theme-context';

function ThemeProvider({ children }) {
  const [mode, setMode] = useState('light');

  const contextValue = {
    mode,
    toggleTheme: () => setMode(m => m === 'light' ? 'dark' : 'light'),
  };

  return (
    <ThemeContext.Provider value={contextValue}>
      {children}
    </ThemeContext.Provider>
  );
}

Note Wrap the Provider at the highest point where descendant components need the value. If the value is a new object on every render, wrap it with useMemo to prevent unnecessary re-renders of consumers.

Consuming Context

syntax
import { useContext } from 'react';
const value = useContext(MyContext);
example
import { useContext } from 'react';
import { ThemeContext } from './theme-context';

function ThemeToggleButton() {
  const { mode, toggleTheme } = useContext(ThemeContext);

  return (
    <button
      onClick={toggleTheme}
      style={{
        background: mode === 'dark' ? '#333' : '#fff',
        color: mode === 'dark' ? '#fff' : '#333',
      }}
    >
      Current: {mode}
    </button>
  );
}

Note useContext always looks for the nearest Provider above the calling component. If none is found, it uses the default value from createContext. The component re-renders whenever the provided value changes.

Multiple Contexts

syntax
<AuthContext.Provider value={auth}>
  <ThemeContext.Provider value={theme}>
    {children}
  </ThemeContext.Provider>
</AuthContext.Provider>
example
function AppProviders({ children }) {
  const auth = useAuthState();
  const theme = useThemeState();
  const locale = useLocaleState();

  return (
    <AuthContext.Provider value={auth}>
      <ThemeContext.Provider value={theme}>
        <LocaleContext.Provider value={locale}>
          {children}
        </LocaleContext.Provider>
      </ThemeContext.Provider>
    </AuthContext.Provider>
  );
}

// Usage at app root
<AppProviders>
  <App />
</AppProviders>

Note Split unrelated data into separate contexts. A single monolithic context forces every consumer to re-render when any part of the value changes, even if they only use a small slice.

Context with State (Custom Hook)

syntax
// Encapsulate context + hook in a module
const Ctx = createContext(null);
export function useMyContext() {
  const ctx = useContext(Ctx);
  if (!ctx) throw new Error('Missing Provider');
  return ctx;
}
example
import { createContext, useContext, useState } from 'react';

const CartContext = createContext(null);

export function CartProvider({ children }) {
  const [items, setItems] = useState([]);

  function addItem(product) {
    setItems(prev => [...prev, { ...product, qty: 1 }]);
  }

  function removeItem(id) {
    setItems(prev => prev.filter(item => item.id !== id));
  }

  const totalItems = items.reduce((sum, i) => sum + i.qty, 0);

  return (
    <CartContext.Provider value={{ items, addItem, removeItem, totalItems }}>
      {children}
    </CartContext.Provider>
  );
}

export function useCart() {
  const context = useContext(CartContext);
  if (!context) {
    throw new Error('useCart must be used within a CartProvider');
  }
  return context;
}

Note Exporting a custom hook that throws when the Provider is missing catches mistakes early and gives a clear error message instead of undefined values that silently break downstream.

Avoiding Unnecessary Re-renders

syntax
// Memoize the value object
const value = useMemo(() => ({ data, handler }), [data]);

// Split read-only data from dispatch
<DataContext.Provider value={data}>
  <DispatchContext.Provider value={dispatch}>
example
import { createContext, useContext, useMemo, useState } from 'react';

const CountContext = createContext(null);
const CountDispatchContext = createContext(null);

function CountProvider({ children }) {
  const [count, setCount] = useState(0);

  // dispatch never changes identity
  const increment = useMemo(
    () => () => setCount(c => c + 1),
    []
  );

  return (
    <CountContext.Provider value={count}>
      <CountDispatchContext.Provider value={increment}>
        {children}
      </CountDispatchContext.Provider>
    </CountContext.Provider>
  );
}

// Components reading only dispatch won't re-render when count changes
function IncrementButton() {
  const increment = useContext(CountDispatchContext);
  return <button onClick={increment}>+1</button>;
}

Note Splitting context into a data context and a dispatch context is a powerful pattern. Components that only call actions (dispatch) will not re-render when the data changes, because the dispatch reference stays stable.

useMemo & useCallback

useMemo

syntax
const memoizedValue = useMemo(() => computeValue(a, b), [a, b]);
example
import { useMemo, useState } from 'react';

function ProductFilter({ products }) {
  const [query, setQuery] = useState('');
  const [sortBy, setSortBy] = useState('name');

  const filtered = useMemo(() => {
    const matched = products.filter(p =>
      p.name.toLowerCase().includes(query.toLowerCase())
    );
    return matched.sort((a, b) =>
      a[sortBy] > b[sortBy] ? 1 : -1
    );
  }, [products, query, sortBy]);

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <select value={sortBy} onChange={e => setSortBy(e.target.value)}>
        <option value="name">Name</option>
        <option value="price">Price</option>
      </select>
      <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>
    </div>
  );
}

Note useMemo caches the result of a computation. React may discard the cache under memory pressure, so never rely on it for correctness -- only for performance. Do not memoize everything by default; profile first.

useCallback

syntax
const memoizedFn = useCallback((args) => {
  doSomething(args, dep);
}, [dep]);
example
import { useCallback, useState } from 'react';

function ParentList({ items }) {
  const [selectedId, setSelectedId] = useState(null);

  const handleSelect = useCallback((id) => {
    setSelectedId(id);
  }, []);

  return (
    <ul>
      {items.map(item => (
        <ListItem
          key={item.id}
          item={item}
          onSelect={handleSelect}
          isSelected={item.id === selectedId}
        />
      ))}
    </ul>
  );
}

Note useCallback(fn, deps) is identical to useMemo(() => fn, deps). Its primary use case is passing stable function references to memoized child components (wrapped with React.memo) to prevent their re-renders.

Dependency Arrays

syntax
// All values from component scope used inside must be listed
useCallback(() => fn(a, b), [a, b]);
useMemo(() => compute(x), [x]);
example
function ChatRoom({ roomId, serverUrl }) {
  // Recreated only when roomId or serverUrl change
  const connect = useCallback(() => {
    const ws = new WebSocket(`${serverUrl}/rooms/${roomId}`);
    ws.onopen = () => console.log('Connected to', roomId);
    return ws;
  }, [roomId, serverUrl]);

  useEffect(() => {
    const ws = connect();
    return () => ws.close();
  }, [connect]);

  return <div>Chat: {roomId}</div>;
}

Note Missing a dependency leads to stale closures. Including an unnecessary one causes pointless recalculations. Use the exhaustive-deps lint rule (eslint-plugin-react-hooks) to catch mistakes automatically.

React.memo

syntax
const MemoizedComponent = React.memo(Component);
// With custom comparison
const MemoizedComponent = React.memo(Component, (prevProps, nextProps) => {
  return prevProps.id === nextProps.id;
});
example
import { memo, useState, useCallback } from 'react';

const ExpensiveRow = memo(function ExpensiveRow({ item, onToggle }) {
  // This component only re-renders if item or onToggle change
  return (
    <tr onClick={() => onToggle(item.id)}>
      <td>{item.name}</td>
      <td>{item.value.toFixed(2)}</td>
    </tr>
  );
});

function DataTable({ rows }) {
  const [selected, setSelected] = useState(new Set());

  const handleToggle = useCallback((id) => {
    setSelected(prev => {
      const next = new Set(prev);
      next.has(id) ? next.delete(id) : next.add(id);
      return next;
    });
  }, []);

  return (
    <table>
      <tbody>
        {rows.map(row => (
          <ExpensiveRow key={row.id} item={row} onToggle={handleToggle} />
        ))}
      </tbody>
    </table>
  );
}

Note React.memo does a shallow comparison of props. It only helps if the component re-renders with the same props frequently. Pair it with useCallback for function props and useMemo for object/array props.

When to Memoize

syntax
// DO memoize when:
// - Computation is genuinely expensive (sorting large arrays, complex math)
// - Passing callbacks/objects to React.memo children
// - Value is a dependency of another hook

// DON'T memoize when:
// - The computation is trivial
// - The component rarely re-renders
// - No downstream consumer cares about reference stability
example
// GOOD: expensive computation
const sortedTransactions = useMemo(
  () => transactions.slice().sort(compareDates),
  [transactions]
);

// GOOD: stable reference for memoized child
const handleDelete = useCallback(
  (id) => dispatch({ type: 'DELETE', id }),
  [dispatch]
);

// UNNECESSARY: trivial computation
// const label = useMemo(() => `Hello ${name}`, [name]);
// Just write: const label = `Hello ${name}`;

Note Memoization has a cost: it uses memory to store results and adds comparison overhead. Only apply it where profiling shows a measurable benefit. Premature memoization adds complexity without improving performance.

Stable References for Effects

syntax
const options = useMemo(() => ({ key: value }), [value]);
useEffect(() => { doSomething(options); }, [options]);
example
function DataFetcher({ endpoint, filters }) {
  // Without useMemo, a new object is created every render,
  // causing the effect to re-run endlessly
  const requestConfig = useMemo(
    () => ({
      url: endpoint,
      params: filters,
      headers: { 'Accept': 'application/json' },
    }),
    [endpoint, filters]
  );

  useEffect(() => {
    const controller = new AbortController();
    fetch(requestConfig.url, {
      ...requestConfig,
      signal: controller.signal,
    }).then(r => r.json()).then(setData);

    return () => controller.abort();
  }, [requestConfig]);
}

Note When an object or array is a dependency of useEffect, memoize it to prevent infinite effect loops. Every render creates a new reference, and useEffect sees a 'change' even when the values inside are identical.

Additional Hooks

useReducer

syntax
const [state, dispatch] = useReducer(reducer, initialState);
// reducer: (state, action) => newState
example
import { useReducer } from 'react';

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return { ...state, items: [...state.items, action.payload] };
    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(i => i.id !== action.payload),
      };
    case 'CLEAR':
      return { ...state, items: [] };
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

function ShoppingCart() {
  const [cart, dispatch] = useReducer(cartReducer, { items: [] });

  return (
    <div>
      <p>{cart.items.length} items</p>
      <button onClick={() =>
        dispatch({ type: 'ADD_ITEM', payload: { id: 1, name: 'Notebook' } })
      }>
        Add Notebook
      </button>
      <button onClick={() => dispatch({ type: 'CLEAR' })}>
        Clear Cart
      </button>
    </div>
  );
}

Note Prefer useReducer over useState when you have complex state logic with multiple sub-values, or when the next state depends on the previous state in varied ways. The reducer function must be pure -- no side effects.

useId

syntax
const id = useId();
example
import { useId } from 'react';

function LabeledInput({ label, type = 'text' }) {
  const id = useId();

  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} type={type} />
    </div>
  );
}

// Each instance gets a unique, stable ID
<LabeledInput label="Username" />
<LabeledInput label="Email" type="email" />

Note useId generates IDs that are stable across server and client renders, preventing hydration mismatches. Never use it for keys in a list. It is meant for accessibility attributes (htmlFor, aria-describedby, etc.).

useDeferredValue

syntax
const deferredValue = useDeferredValue(value);
example
import { useState, useDeferredValue, useMemo } from 'react';

function HeavySearch({ allItems }) {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  const results = useMemo(
    () => allItems.filter(item =>
      item.name.toLowerCase().includes(deferredQuery.toLowerCase())
    ),
    [allItems, deferredQuery]
  );

  const isStale = query !== deferredQuery;

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <div style={{ opacity: isStale ? 0.6 : 1 }}>
        {results.map(item => <p key={item.id}>{item.name}</p>)}
      </div>
    </div>
  );
}

Note useDeferredValue lets the UI stay responsive by deferring re-rendering of expensive parts. It is similar to debouncing but integrated with React's rendering scheduler. Compare the deferred and current value to show a stale indicator.

useTransition

syntax
const [isPending, startTransition] = useTransition();
example
import { useState, useTransition } from 'react';

function TabContainer() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }

  return (
    <div>
      <nav>
        <button onClick={() => selectTab('home')}>Home</button>
        <button onClick={() => selectTab('analytics')}>Analytics</button>
        <button onClick={() => selectTab('settings')}>Settings</button>
      </nav>
      {isPending && <p>Loading...</p>}
      <TabPanel activeTab={tab} />
    </div>
  );
}

Note startTransition marks state updates as non-urgent, allowing urgent updates (like typing) to interrupt them. The isPending flag lets you show loading indicators. Unlike useDeferredValue, useTransition wraps the state update itself.

useSyncExternalStore

syntax
const snapshot = useSyncExternalStore(
  subscribe,
  getSnapshot,
  getServerSnapshot? // optional, for SSR
);
example
import { useSyncExternalStore } from 'react';

function useOnlineStatus() {
  return useSyncExternalStore(
    (notify) => {
      window.addEventListener('online', notify);
      window.addEventListener('offline', notify);
      return () => {
        window.removeEventListener('online', notify);
        window.removeEventListener('offline', notify);
      };
    },
    () => navigator.onLine,  // client snapshot
    () => true               // server snapshot (assume online)
  );
}

function StatusBar() {
  const isOnline = useOnlineStatus();
  return <p>{isOnline ? 'Connected' : 'Offline'}</p>;
}

Note Designed for subscribing to external data sources (browser APIs, third-party stores) in a way that is compatible with concurrent rendering. The subscribe function must accept a callback and return an unsubscribe function.

useDebugValue

syntax
useDebugValue(value);
useDebugValue(value, formatFn); // lazy formatting
example
import { useDebugValue, useSyncExternalStore } from 'react';

function useMediaQuery(query) {
  const matches = useSyncExternalStore(
    (notify) => {
      const mql = window.matchMedia(query);
      mql.addEventListener('change', notify);
      return () => mql.removeEventListener('change', notify);
    },
    () => window.matchMedia(query).matches,
    () => false
  );

  // Shows in React DevTools next to the hook
  useDebugValue(matches ? `Matches: ${query}` : `No match: ${query}`);

  return matches;
}

Note useDebugValue only affects the React DevTools display for custom hooks. It adds no runtime behavior. Use the format function (second argument) to defer expensive string formatting until the hook is actually inspected in DevTools.

React 19 Features

use() Hook

syntax
const value = use(resource);
// resource can be a Promise or a Context
example
import { use, Suspense } from 'react';

function UserGreeting({ userPromise }) {
  // React suspends until the promise resolves
  const user = use(userPromise);
  return <h2>Hello, {user.name}!</h2>;
}

function App() {
  const userPromise = fetchCurrentUser();

  return (
    <Suspense fallback={<p>Loading user...</p>}>
      <UserGreeting userPromise={userPromise} />
    </Suspense>
  );
}

Note Unlike other hooks, use() can be called inside if statements and loops. When reading a promise, wrap the component in Suspense. The promise should be created outside the component or cached -- do not create new promises inside render.

use() with Context

syntax
const value = use(SomeContext);
// Can be called conditionally, unlike useContext
example
import { use, createContext } from 'react';

const FeatureFlagContext = createContext({ darkMode: false });

function Banner({ showThemed }) {
  // Reading context conditionally -- not possible with useContext
  if (showThemed) {
    const { darkMode } = use(FeatureFlagContext);
    return (
      <div style={{ background: darkMode ? '#222' : '#eee' }}>
        Themed banner
      </div>
    );
  }
  return <div>Standard banner</div>;
}

Note use(Context) works like useContext but can be placed after early returns or inside conditionals. This is useful when you only need the context value in some code paths.

Actions (Form Actions)

syntax
<form action={actionFn}>
  ...
</form>
// actionFn receives FormData automatically
example
function ContactForm() {
  async function submitContact(formData) {
    const name = formData.get('name');
    const email = formData.get('email');
    await sendContactRequest({ name, email });
  }

  return (
    <form action={submitContact}>
      <input name="name" placeholder="Your name" required />
      <input name="email" type="email" placeholder="Email" required />
      <button type="submit">Send</button>
    </form>
  );
}

Note Actions are async functions passed to the action prop of <form>. They receive FormData as an argument. React handles the pending state and error transitions automatically. Actions work with useActionState and useFormStatus for richer patterns.

useFormStatus

syntax
const { pending, data, method, action } = useFormStatus();
example
import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Saving...' : 'Save Changes'}
    </button>
  );
}

function EditProfile() {
  async function updateProfile(formData) {
    await saveProfile(Object.fromEntries(formData));
  }

  return (
    <form action={updateProfile}>
      <input name="displayName" placeholder="Display name" />
      <input name="location" placeholder="Location" />
      <SubmitButton />
    </form>
  );
}

Note useFormStatus must be called from a component rendered inside a <form>. It will not work if called in the same component that renders the form -- it reads from the nearest parent form. Import it from 'react-dom', not 'react'.

useOptimistic

syntax
const [optimisticState, setOptimistic] = useOptimistic(
  actualState,
  updateFn? // (currentState, optimisticValue) => newState
);
example
import { useOptimistic, useState } from 'react';

function MessageThread({ initialMessages }) {
  const [messages, setMessages] = useState(initialMessages);
  const [optimisticMessages, addOptimistic] = useOptimistic(
    messages,
    (current, newMsg) => [...current, { ...newMsg, sending: true }]
  );

  async function sendMessage(formData) {
    const text = formData.get('message');
    const tempMsg = { id: Date.now(), text, sending: true };
    addOptimistic(tempMsg);

    const saved = await postMessage(text);
    setMessages(prev => [...prev, saved]);
  }

  return (
    <div>
      {optimisticMessages.map(msg => (
        <p key={msg.id} style={{ opacity: msg.sending ? 0.6 : 1 }}>
          {msg.text}
        </p>
      ))}
      <form action={sendMessage}>
        <input name="message" />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

Note useOptimistic shows a temporary state while an async action is in progress. When the action completes (or fails), React reverts to the actual state. The optimistic value is automatically rolled back if the action throws.

useActionState

syntax
const [state, formAction, isPending] = useActionState(
  actionFn,
  initialState
);
// actionFn: (previousState, formData) => newState
example
import { useActionState } from 'react';

function LoginForm() {
  const [state, loginAction, isPending] = useActionState(
    async (prevState, formData) => {
      const email = formData.get('email');
      const password = formData.get('password');

      try {
        await authenticateUser(email, password);
        return { error: null, success: true };
      } catch (err) {
        return { error: err.message, success: false };
      }
    },
    { error: null, success: false }
  );

  return (
    <form action={loginAction}>
      <input name="email" type="email" required />
      <input name="password" type="password" required />
      {state.error && <p className="error">{state.error}</p>}
      <button disabled={isPending}>
        {isPending ? 'Signing in...' : 'Sign In'}
      </button>
    </form>
  );
}

Note useActionState combines action handling with state management. The action function receives the previous state as its first argument and FormData as its second. The third return value (isPending) tells you if the action is running.

ref as a Prop

syntax
// React 19: no forwardRef needed
function MyComponent({ ref, ...props }) {
  return <div ref={ref} {...props} />;
}
example
function TextArea({ ref, label, ...rest }) {
  return (
    <div className="field">
      <label>{label}</label>
      <textarea ref={ref} {...rest} />
    </div>
  );
}

function CommentForm() {
  const textareaRef = useRef(null);

  function handleReset() {
    textareaRef.current.value = '';
    textareaRef.current.focus();
  }

  return (
    <div>
      <TextArea ref={textareaRef} label="Comment" rows={4} />
      <button onClick={handleReset}>Clear</button>
    </div>
  );
}

Note React 19 passes ref as a regular prop to function components. forwardRef is deprecated -- migrate by moving ref into the props destructuring. Existing forwardRef code continues to work but will show deprecation warnings.

Document Metadata

syntax
// Render <title>, <meta>, <link> directly in components
function Page() {
  return (
    <>
      <title>Page Title</title>
      <meta name="description" content="..." />
      {/* page content */}
    </>
  );
}
example
function ProductPage({ product }) {
  return (
    <article>
      <title>{product.name} | ShopApp</title>
      <meta name="description" content={product.summary} />
      <link rel="canonical" href={`/products/${product.slug}`} />

      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <strong>${product.price}</strong>
    </article>
  );
}

// React hoists these tags into the <head> automatically

Note In React 19, metadata tags rendered inside components are automatically hoisted into the document <head>. This eliminates the need for third-party helmet libraries for basic metadata management. Works with SSR and client rendering.

Forms

Controlled Inputs

syntax
<input value={state} onChange={e => setState(e.target.value)} />
example
function EmailInput() {
  const [email, setEmail] = useState('');
  const isValid = email.includes('@') && email.includes('.');

  return (
    <div>
      <input
        type="email"
        value={email}
        onChange={e => setEmail(e.target.value)}
        className={isValid ? 'valid' : 'invalid'}
      />
      {!isValid && email.length > 0 && (
        <small>Please enter a valid email address</small>
      )}
    </div>
  );
}

Note Controlled inputs keep React as the single source of truth. The value prop locks the input to the state. If you set value without onChange, the input becomes read-only.

Uncontrolled Inputs

syntax
<input defaultValue={initial} ref={inputRef} />
// Read with: inputRef.current.value
example
function QuickSearch() {
  const searchRef = useRef(null);

  function handleSubmit(e) {
    e.preventDefault();
    const query = searchRef.current.value;
    performSearch(query);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input ref={searchRef} defaultValue="" placeholder="Search..." />
      <button type="submit">Go</button>
    </form>
  );
}

Note Use defaultValue (not value) for uncontrolled inputs. The DOM holds the state. Uncontrolled inputs are simpler for cases where you only need the value at submission time. For dynamic validation or conditional logic, controlled inputs are better.

Form Submission

syntax
// Traditional
<form onSubmit={handleSubmit}>
// React 19 Actions
<form action={actionFn}>
example
// Traditional approach
function FeedbackForm() {
  const [message, setMessage] = useState('');
  const [submitted, setSubmitted] = useState(false);

  async function handleSubmit(e) {
    e.preventDefault();
    await sendFeedback(message);
    setSubmitted(true);
    setMessage('');
  }

  if (submitted) return <p>Thank you for your feedback!</p>;

  return (
    <form onSubmit={handleSubmit}>
      <textarea
        value={message}
        onChange={e => setMessage(e.target.value)}
        required
      />
      <button type="submit">Submit</button>
    </form>
  );
}

Note With onSubmit, always call e.preventDefault() to stop the browser from reloading the page. With React 19 form actions, preventDefault is handled automatically. Both patterns are valid -- actions are newer and reduce boilerplate.

Validation Patterns

syntax
// Validate on change, blur, or submit
const errors = {};
if (!value) errors.field = 'Required';
example
function SignupForm() {
  const [fields, setFields] = useState({ name: '', email: '', age: '' });
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  function validate(data) {
    const errs = {};
    if (!data.name.trim()) errs.name = 'Name is required';
    if (!data.email.includes('@')) errs.email = 'Invalid email';
    if (Number(data.age) < 18) errs.age = 'Must be at least 18';
    return errs;
  }

  function handleChange(field, value) {
    const updated = { ...fields, [field]: value };
    setFields(updated);
    if (touched[field]) {
      setErrors(validate(updated));
    }
  }

  function handleBlur(field) {
    setTouched(prev => ({ ...prev, [field]: true }));
    setErrors(validate(fields));
  }

  function handleSubmit(e) {
    e.preventDefault();
    const errs = validate(fields);
    setErrors(errs);
    setTouched({ name: true, email: true, age: true });
    if (Object.keys(errs).length === 0) {
      submitSignup(fields);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={fields.name}
        onChange={e => handleChange('name', e.target.value)}
        onBlur={() => handleBlur('name')}
      />
      {touched.name && errors.name && <span>{errors.name}</span>}
      {/* repeat for other fields */}
      <button type="submit">Sign Up</button>
    </form>
  );
}

Note Validate on blur for a good UX: users see errors after leaving a field, not while typing. On submit, validate everything and mark all fields as touched. For complex forms, consider a form library or useReducer.

Multiple Inputs with One Handler

syntax
function handleChange(e) {
  const { name, value } = e.target;
  setForm(prev => ({ ...prev, [name]: value }));
}
example
function AddressForm() {
  const [address, setAddress] = useState({
    street: '',
    city: '',
    state: '',
    zip: '',
  });

  function handleChange(e) {
    const { name, value } = e.target;
    setAddress(prev => ({ ...prev, [name]: value }));
  }

  return (
    <form>
      <input name="street" value={address.street} onChange={handleChange} placeholder="Street" />
      <input name="city" value={address.city} onChange={handleChange} placeholder="City" />
      <input name="state" value={address.state} onChange={handleChange} placeholder="State" />
      <input name="zip" value={address.zip} onChange={handleChange} placeholder="ZIP" />
    </form>
  );
}

Note Use the name attribute on inputs and a computed property name ([name]) to handle many fields with one function. This scales well and avoids writing separate handlers for each field.

Select, Checkbox & Radio

syntax
// Select
<select value={state} onChange={handler}>...
// Checkbox
<input type="checkbox" checked={state} onChange={handler} />
// Radio
<input type="radio" value="opt" checked={state === 'opt'} onChange={handler} />
example
function PreferencesForm() {
  const [color, setColor] = useState('blue');
  const [newsletter, setNewsletter] = useState(false);
  const [plan, setPlan] = useState('free');

  return (
    <form>
      {/* Select */}
      <label>Favorite Color</label>
      <select value={color} onChange={e => setColor(e.target.value)}>
        <option value="blue">Blue</option>
        <option value="green">Green</option>
        <option value="red">Red</option>
      </select>

      {/* Checkbox */}
      <label>
        <input
          type="checkbox"
          checked={newsletter}
          onChange={e => setNewsletter(e.target.checked)}
        />
        Subscribe to newsletter
      </label>

      {/* Radio */}
      {['free', 'pro', 'enterprise'].map(option => (
        <label key={option}>
          <input
            type="radio"
            name="plan"
            value={option}
            checked={plan === option}
            onChange={e => setPlan(e.target.value)}
          />
          {option}
        </label>
      ))}
    </form>
  );
}

Note For checkboxes, read e.target.checked (boolean), not e.target.value. For selects, value goes on the <select> element, not on individual <option> elements. Radio buttons in the same group share the same name attribute.

Event Handling

Click Events

syntax
<button onClick={handler}>...</button>
// With argument
<button onClick={() => handler(arg)}>...</button>
example
function ColorPicker({ colors, onSelect }) {
  return (
    <div className="palette">
      {colors.map(color => (
        <button
          key={color}
          style={{ backgroundColor: color, width: 40, height: 40 }}
          onClick={() => onSelect(color)}
          aria-label={`Select ${color}`}
        />
      ))}
    </div>
  );
}

Note Pass a function reference, not a function call. Writing onClick={handler()} would call the function during render. Use an arrow function when you need to pass arguments: onClick={() => handler(id)}.

Change Events

syntax
<input onChange={(e) => handleChange(e.target.value)} />
<select onChange={(e) => handleSelect(e.target.value)}>
example
function TemperatureConverter() {
  const [celsius, setCelsius] = useState('');
  const fahrenheit = celsius !== '' ? (Number(celsius) * 9/5 + 32).toFixed(1) : '';

  return (
    <div>
      <label>
        Celsius:
        <input
          type="number"
          value={celsius}
          onChange={e => setCelsius(e.target.value)}
        />
      </label>
      <p>Fahrenheit: {fahrenheit}</p>
    </div>
  );
}

Note React's onChange fires on every keystroke, not just when the field loses focus (which is the native DOM behavior). This makes it ideal for real-time validation and derived values.

Submit Events

syntax
<form onSubmit={handleSubmit}>
  ...
  <button type="submit">Submit</button>
</form>
example
function SearchForm({ onSearch }) {
  const [query, setQuery] = useState('');

  function handleSubmit(e) {
    e.preventDefault();
    if (query.trim()) {
      onSearch(query.trim());
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search articles..."
      />
      <button type="submit">Search</button>
    </form>
  );
}

Note Always call e.preventDefault() in onSubmit to prevent the browser from performing a full page reload. The submit event fires when pressing Enter inside an input or clicking a submit button.

Synthetic Event Object

syntax
function handler(event) {
  event.target       // the DOM element
  event.currentTarget // element the handler is attached to
  event.preventDefault()
  event.stopPropagation()
  event.nativeEvent   // underlying browser event
}
example
function LinkInterceptor({ children }) {
  function handleClick(e) {
    // Check if it's an internal link
    const href = e.target.closest('a')?.getAttribute('href');
    if (href?.startsWith('/')) {
      e.preventDefault();
      navigate(href); // client-side navigation
    }
  }

  return <div onClick={handleClick}>{children}</div>;
}

Note React wraps native events in SyntheticEvent objects that work identically across browsers. As of React 17+, events are no longer pooled, so you can access event properties asynchronously without calling e.persist().

Preventing Default & Propagation

syntax
e.preventDefault();    // stop default browser action
e.stopPropagation();   // stop bubbling to parent handlers
example
function DropZone({ onFileDrop }) {
  function handleDragOver(e) {
    e.preventDefault(); // Required to allow drop
  }

  function handleDrop(e) {
    e.preventDefault();
    e.stopPropagation();
    const files = Array.from(e.dataTransfer.files);
    onFileDrop(files);
  }

  return (
    <div
      onDragOver={handleDragOver}
      onDrop={handleDrop}
      className="drop-zone"
    >
      Drop files here
    </div>
  );
}

Note Use preventDefault for links, form submissions, and drag-and-drop. Use stopPropagation sparingly -- it makes debugging harder. Prefer checking the event target to decide whether to act instead of stopping propagation.

Capture Phase & Delegation

syntax
// Capture phase (fires before children)
<div onClickCapture={handler}>...</div>

// Event delegation: one handler on parent
<ul onClick={handleItemClick}>
  <li data-id="1">A</li>
  <li data-id="2">B</li>
</ul>
example
function ItemGrid({ items, onItemAction }) {
  function handleClick(e) {
    const itemEl = e.target.closest('[data-item-id]');
    if (!itemEl) return;

    const id = itemEl.dataset.itemId;
    const action = e.target.dataset.action;

    if (action === 'delete') {
      onItemAction(id, 'delete');
    } else if (action === 'edit') {
      onItemAction(id, 'edit');
    }
  }

  return (
    <div onClick={handleClick}>
      {items.map(item => (
        <div key={item.id} data-item-id={item.id}>
          <span>{item.name}</span>
          <button data-action="edit">Edit</button>
          <button data-action="delete">Delete</button>
        </div>
      ))}
    </div>
  );
}

Note Event delegation (one handler on a parent) reduces the number of event listeners and works well for dynamic lists. Use data-* attributes or closest() to identify which child triggered the event. React internally uses delegation at the root already.

Patterns

Custom Hooks

syntax
function useCustomHook(params) {
  // use built-in hooks inside
  return result;
}
example
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored !== null ? JSON.parse(stored) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

// Usage
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      Theme: {theme}
    </button>
  );
}

Note Custom hooks must start with 'use' so React can enforce the rules of hooks. They let you extract and share stateful logic between components without changing the component hierarchy.

Compound Components

syntax
function Parent({ children }) { ... }
Parent.Child = function Child(props) { ... };
example
const AccordionContext = createContext(null);

function Accordion({ children }) {
  const [openIndex, setOpenIndex] = useState(null);
  return (
    <AccordionContext.Provider value={{ openIndex, setOpenIndex }}>
      <div className="accordion">{children}</div>
    </AccordionContext.Provider>
  );
}

function AccordionItem({ index, title, children }) {
  const { openIndex, setOpenIndex } = useContext(AccordionContext);
  const isOpen = openIndex === index;

  return (
    <div>
      <button onClick={() => setOpenIndex(isOpen ? null : index)}>
        {title} {isOpen ? '−' : '+'}
      </button>
      {isOpen && <div className="panel">{children}</div>}
    </div>
  );
}

Accordion.Item = AccordionItem;

// Usage
<Accordion>
  <Accordion.Item index={0} title="Section A">Content A</Accordion.Item>
  <Accordion.Item index={1} title="Section B">Content B</Accordion.Item>
</Accordion>

Note Compound components share implicit state via context, letting you create flexible APIs like <Select> + <Select.Option>. The parent manages state and children consume it without prop drilling.

Render Props

syntax
function DataProvider({ render }) {
  const data = useData();
  return render(data);
}
// or via children
<DataProvider>{(data) => <View data={data} />}</DataProvider>
example
function MouseTracker({ children }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

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

  return children(position);
}

// Usage
<MouseTracker>
  {({ x, y }) => (
    <div>
      Cursor is at ({x}, {y})
    </div>
  )}
</MouseTracker>

Note Render props are largely replaced by custom hooks, which are simpler. Still useful when you need to control what renders based on shared logic without creating a custom hook for every case.

Error Boundaries

syntax
// Error boundaries must be class components (as of React 19)
class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  componentDidCatch(error, info) { logError(error, info); }
  render() {
    if (this.state.hasError) return <Fallback />;
    return this.props.children;
  }
}
example
class AppErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    reportToService(error, errorInfo.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-screen">
          <h1>Something went wrong</h1>
          <button onClick={() => this.setState({ hasError: false })}>
            Try again
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

// Wrap sections of the UI
<AppErrorBoundary>
  <Dashboard />
</AppErrorBoundary>

Note Error boundaries catch errors during rendering, in lifecycle methods, and in constructors of the tree below them. They do NOT catch errors in event handlers, async code, or server-side rendering. Use try/catch for those.

Portals

syntax
import { createPortal } from 'react-dom';
createPortal(children, domNode);
example
import { createPortal } from 'react-dom';

function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;

  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={e => e.stopPropagation()}>
        <button className="close-btn" onClick={onClose}>X</button>
        {children}
      </div>
    </div>,
    document.getElementById('modal-root')
  );
}

// Usage
function App() {
  const [showModal, setShowModal] = useState(false);
  return (
    <div>
      <button onClick={() => setShowModal(true)}>Open</button>
      <Modal isOpen={showModal} onClose={() => setShowModal(false)}>
        <h2>Modal Title</h2>
        <p>Modal body content here.</p>
      </Modal>
    </div>
  );
}

Note Portals render children into a different DOM node outside the parent hierarchy, but events still bubble through the React tree (not the DOM tree). Perfect for modals, tooltips, and dropdowns that need to escape overflow:hidden or z-index contexts.

Suspense & Lazy Loading

syntax
const LazyComponent = React.lazy(() => import('./Component'));

<Suspense fallback={<Loading />}>
  <LazyComponent />
</Suspense>
example
import { lazy, Suspense, useState } from 'react';

const AdminPanel = lazy(() => import('./AdminPanel'));
const UserDashboard = lazy(() => import('./UserDashboard'));

function App({ role }) {
  return (
    <Suspense fallback={<div className="spinner">Loading...</div>}>
      {role === 'admin' ? <AdminPanel /> : <UserDashboard />}
    </Suspense>
  );
}

Note React.lazy only works with default exports. For named exports, create an intermediate module that re-exports as default. Suspense can also wrap components that use the use() hook to read promises in React 19.

Context + Reducer Pattern

syntax
const StateCtx = createContext(null);
const DispatchCtx = createContext(null);

function Provider({ children }) {
  const [state, dispatch] = useReducer(reducer, init);
  return (
    <StateCtx.Provider value={state}>
      <DispatchCtx.Provider value={dispatch}>
        {children}
      </DispatchCtx.Provider>
    </StateCtx.Provider>
  );
}
example
const TodoStateContext = createContext(null);
const TodoDispatchContext = createContext(null);

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return [...state, { id: crypto.randomUUID(), text: action.text, done: false }];
    case 'TOGGLE':
      return state.map(t =>
        t.id === action.id ? { ...t, done: !t.done } : t
      );
    case 'DELETE':
      return state.filter(t => t.id !== action.id);
    default:
      throw new Error(`Unhandled: ${action.type}`);
  }
}

function TodoProvider({ children }) {
  const [todos, dispatch] = useReducer(todoReducer, []);
  return (
    <TodoStateContext.Provider value={todos}>
      <TodoDispatchContext.Provider value={dispatch}>
        {children}
      </TodoDispatchContext.Provider>
    </TodoStateContext.Provider>
  );
}

function useTodos() {
  return useContext(TodoStateContext);
}
function useTodoDispatch() {
  return useContext(TodoDispatchContext);
}

Note This pattern provides Redux-like state management with built-in React APIs. Splitting state and dispatch into separate contexts prevents components that only dispatch actions from re-rendering when state changes.

Higher-Order Components (HOC)

syntax
function withFeature(WrappedComponent) {
  return function EnhancedComponent(props) {
    const data = useFeatureData();
    return <WrappedComponent {...props} data={data} />;
  };
}
example
function withAuth(Component) {
  return function AuthenticatedComponent(props) {
    const { user, loading } = useAuth();

    if (loading) return <p>Authenticating...</p>;
    if (!user) return <LoginRedirect />;

    return <Component {...props} user={user} />;
  };
}

// Usage
const ProtectedDashboard = withAuth(Dashboard);

function App() {
  return <ProtectedDashboard showStats />;
}

Note HOCs were the primary code reuse pattern before hooks. Custom hooks are now preferred for most cases because they are simpler and avoid the wrapper component nesting. HOCs are still useful for cross-cutting concerns like auth guards.

State Management

Lifting State Up

syntax
// Move shared state to the nearest common ancestor
function Parent() {
  const [shared, setShared] = useState(init);
  return (
    <>
      <ChildA value={shared} />
      <ChildB onChange={setShared} />
    </>
  );
}
example
function TemperatureCalculator() {
  const [temp, setTemp] = useState('');
  const [scale, setScale] = useState('c');

  const celsius = scale === 'f' ? ((Number(temp) - 32) * 5/9).toFixed(1) : temp;
  const fahrenheit = scale === 'c' ? ((Number(temp) * 9/5) + 32).toFixed(1) : temp;

  return (
    <div>
      <TemperatureInput
        label="Celsius"
        value={celsius}
        onChange={val => { setTemp(val); setScale('c'); }}
      />
      <TemperatureInput
        label="Fahrenheit"
        value={fahrenheit}
        onChange={val => { setTemp(val); setScale('f'); }}
      />
    </div>
  );
}

function TemperatureInput({ label, value, onChange }) {
  return (
    <label>
      {label}:
      <input value={value} onChange={e => onChange(e.target.value)} />
    </label>
  );
}

Note When two components need the same data, move the state to their closest shared parent and pass it down as props. This is the fundamental React data flow pattern.

Prop Drilling Solutions

syntax
// 1. Context
// 2. Composition (pass components, not data)
// 3. Component composition with children
example
// PROBLEM: drilling "user" through many layers
// <App user={user}> -> <Layout user> -> <Header user> -> <Avatar user>

// SOLUTION 1: Context
const UserContext = createContext(null);
// provide at top, consume at Avatar

// SOLUTION 2: Composition -- pass the rendered element
function App({ user }) {
  const avatar = <Avatar user={user} />;
  return <Layout header={<Header avatar={avatar} />} />;
}

// Now Layout and Header don't need to know about "user" at all
function Layout({ header }) {
  return (
    <div>
      <nav>{header}</nav>
      <main>...</main>
    </div>
  );
}

function Header({ avatar }) {
  return <div className="header">{avatar}</div>;
}

Note Before reaching for context, try component composition. Passing pre-rendered elements as props skips intermediate layers entirely and keeps the data flow explicit. Context is better for truly global data (theme, auth, locale).

Context for Global State

syntax
// Create -> Provide -> Consume
const Ctx = createContext(defaultValue);
<Ctx.Provider value={value}>...
const value = useContext(Ctx);
example
// Locale context for internationalization
const LocaleContext = createContext('en');

function LocaleProvider({ children }) {
  const [locale, setLocale] = useState('en');

  const translations = useMemo(
    () => loadTranslations(locale),
    [locale]
  );

  const value = useMemo(
    () => ({ locale, setLocale, t: (key) => translations[key] || key }),
    [locale, translations]
  );

  return (
    <LocaleContext.Provider value={value}>
      {children}
    </LocaleContext.Provider>
  );
}

function useLocale() {
  return useContext(LocaleContext);
}

// Usage in any component
function WelcomeBanner() {
  const { t, locale } = useLocale();
  return <h1>{t('welcome_message')} ({locale})</h1>;
}

Note Context is ideal for low-frequency updates like theme, locale, and auth. For high-frequency updates (e.g., mouse position), context can cause excessive re-renders -- consider useSyncExternalStore or a dedicated state library.

When to Use External State

syntax
// Consider external state management when:
// - Many components read/write the same state
// - State updates are frequent and complex
// - You need middleware (logging, persistence, devtools)
// - Server state caching is needed (React Query, SWR)
example
// Signs you might need a state library:

// 1. Context re-renders are a bottleneck
// function App() {
//   return (
//     <StoreProvider>  {/* every consumer re-renders on any change */}
//       <BigComponentTree />
//     </StoreProvider>
//   );
// }

// 2. You need selectors (subscribe to a slice)
// const count = useStore(state => state.count);

// 3. You need server state cache with stale-while-revalidate
// const { data, isLoading } = useQuery('users', fetchUsers);

// Popular choices:
// - Zustand: lightweight, hook-based, selectors
// - Jotai: atomic state, bottom-up approach
// - React Query / TanStack Query: server state management
// - Redux Toolkit: complex client state with devtools

Note Start with useState and useReducer + context. Only add an external library when you hit a concrete pain point: performance issues from context re-renders, complex async logic, or the need for devtools and middleware.

Derived State

syntax
// Compute from existing state during render
const derivedValue = computeFrom(stateA, stateB);
// Do NOT store derived values in separate state
example
function ShoppingCart({ items }) {
  // GOOD: derive during render
  const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
  const tax = subtotal * 0.08;
  const total = subtotal + tax;
  const itemCount = items.reduce((sum, item) => sum + item.qty, 0);

  return (
    <div>
      <p>{itemCount} items</p>
      <p>Subtotal: ${subtotal.toFixed(2)}</p>
      <p>Tax: ${tax.toFixed(2)}</p>
      <p>Total: ${total.toFixed(2)}</p>
    </div>
  );

  // BAD: storing derived data in state
  // const [total, setTotal] = useState(0);
  // useEffect(() => setTotal(subtotal + tax), [subtotal, tax]);
}

Note If you can compute a value from existing state or props, derive it inline during render instead of syncing it with useState + useEffect. Derived state in useEffect causes an extra render cycle and introduces potential bugs.

State Colocation

syntax
// Keep state as close to where it is used as possible
// Move state DOWN if only one child needs it
// Move state UP only when siblings need to share it
example
// BAD: state lives too high
function App() {
  const [searchQuery, setSearchQuery] = useState('');
  return (
    <Layout>
      <Header />
      <SearchPanel query={searchQuery} setQuery={setSearchQuery} />
      <Footer />
    </Layout>
  );
}

// GOOD: state lives in the component that uses it
function App() {
  return (
    <Layout>
      <Header />
      <SearchPanel />
      <Footer />
    </Layout>
  );
}

function SearchPanel() {
  const [searchQuery, setSearchQuery] = useState('');
  const results = useSearchResults(searchQuery);
  return (
    <div>
      <input value={searchQuery} onChange={e => setSearchQuery(e.target.value)} />
      <ResultsList results={results} />
    </div>
  );
}

Note Colocating state reduces unnecessary re-renders and makes components easier to understand. Only lift state when it genuinely needs to be shared. Global state for local concerns is an anti-pattern that hurts performance and readability.

Styling

Inline Styles

syntax
<div style={{ cssProperty: value }} />
example
function AlertBanner({ type, message }) {
  const styles = {
    padding: '12px 16px',
    borderRadius: '6px',
    fontWeight: 'bold',
    backgroundColor: type === 'error' ? '#fde8e8' : '#e8f5e9',
    color: type === 'error' ? '#c62828' : '#2e7d32',
    border: `1px solid ${type === 'error' ? '#ef9a9a' : '#a5d6a7'}`,
  };

  return <div style={styles}>{message}</div>;
}

Note Inline styles use camelCase property names and string/number values. They cannot handle pseudo-classes (:hover), media queries, or keyframe animations. Best for truly dynamic values that change at runtime.

CSS Modules

syntax
import styles from './Component.module.css';
<div className={styles.container} />
example
// Button.module.css
// .button { padding: 8px 16px; border-radius: 4px; }
// .primary { background: #1976d2; color: white; }
// .secondary { background: #e0e0e0; color: #333; }

import styles from './Button.module.css';

function Button({ variant = 'primary', children, ...rest }) {
  const className = `${styles.button} ${styles[variant]}`;

  return (
    <button className={className} {...rest}>
      {children}
    </button>
  );
}

Note CSS Modules scope class names locally by default, preventing naming collisions. The imported styles object maps your authored class names to unique generated ones. Supported out of the box by most React build tools.

className Patterns

syntax
<div className="static-class" />
<div className={dynamicVariable} />
<div className={`base ${condition ? 'active' : ''}`} />
example
function NavLink({ href, isActive, children }) {
  // Template literal approach
  const className = `nav-link ${isActive ? 'nav-link--active' : ''}`;

  return <a href={href} className={className}>{children}</a>;
}

// Helper function for multiple conditional classes
function classNames(...classes) {
  return classes.filter(Boolean).join(' ');
}

function Card({ featured, compact, className }) {
  return (
    <div
      className={classNames(
        'card',
        featured && 'card--featured',
        compact && 'card--compact',
        className
      )}
    >
      {/* ... */}
    </div>
  );
}

Note Use a small classNames helper or the popular 'clsx' package to compose conditional classes cleanly. Avoid complex ternary chains inside className attributes -- extract the logic to keep JSX readable.

Conditional Classes

syntax
// With clsx or classnames library
import clsx from 'clsx';
<div className={clsx('base', { active: isActive, disabled: isDisabled })} />
example
// Using the clsx library
import clsx from 'clsx';

function StatusChip({ status }) {
  return (
    <span
      className={clsx('chip', {
        'chip--success': status === 'active',
        'chip--warning': status === 'pending',
        'chip--danger': status === 'error',
        'chip--muted': status === 'inactive',
      })}
    >
      {status}
    </span>
  );
}

// Without a library
function StatusChipManual({ status }) {
  const chipClass = [
    'chip',
    status === 'active' && 'chip--success',
    status === 'error' && 'chip--danger',
  ].filter(Boolean).join(' ');

  return <span className={chipClass}>{status}</span>;
}

Note The clsx library (or classnames) accepts strings, objects, and arrays. Object keys are included when their value is truthy. It is lightweight (~228 bytes) and widely used in the React ecosystem.

CSS-in-JS Patterns

syntax
// styled-components / Emotion example pattern
const StyledButton = styled.button`
  background: ${props => props.primary ? 'blue' : 'gray'};
  color: white;
`;
example
// Using a CSS-in-JS library (styled-components pattern)
import styled from 'styled-components';

const Container = styled.div`
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
`;

const Title = styled.h1`
  font-size: ${props => props.large ? '2.5rem' : '1.5rem'};
  color: ${props => props.muted ? '#888' : '#111'};
  margin-bottom: 16px;
`;

function ArticlePage({ article }) {
  return (
    <Container>
      <Title large>{article.title}</Title>
      <p>{article.body}</p>
    </Container>
  );
}

Note CSS-in-JS co-locates styles with components and supports dynamic props, theming, and automatic vendor prefixing. The tradeoff is runtime overhead and larger bundle size. Consider CSS Modules or Tailwind for better performance in critical paths.

Utility-First CSS (Tailwind Pattern)

syntax
<div className="flex items-center gap-4 p-4 bg-white rounded-lg shadow" />
example
function UserCard({ user }) {
  return (
    <div className="flex items-center gap-4 p-4 bg-white rounded-lg shadow-sm border">
      <img
        src={user.avatarUrl}
        alt={user.name}
        className="w-12 h-12 rounded-full object-cover"
      />
      <div>
        <h3 className="font-semibold text-gray-900">{user.name}</h3>
        <p className="text-sm text-gray-500">{user.role}</p>
      </div>
      <span
        className={`ml-auto px-2 py-1 text-xs rounded-full ${
          user.active
            ? 'bg-green-100 text-green-800'
            : 'bg-gray-100 text-gray-600'
        }`}
      >
        {user.active ? 'Active' : 'Offline'}
      </span>
    </div>
  );
}

Note Utility-first CSS like Tailwind works well with React's component model -- styles are scoped per component by nature. Use clsx for conditional classes. Extract repeated patterns into components rather than creating custom CSS utility classes.

Performance

React.memo for Component Memoization

syntax
const Memoized = React.memo(Component);
// Re-renders only when props change (shallow comparison)
example
import { memo } from 'react';

const ChatMessage = memo(function ChatMessage({ author, text, timestamp }) {
  // This only re-renders when author, text, or timestamp change
  return (
    <div className="message">
      <strong>{author}</strong>
      <p>{text}</p>
      <time>{new Date(timestamp).toLocaleTimeString()}</time>
    </div>
  );
});

// Parent re-renders frequently (new messages arrive)
// but existing ChatMessage components skip re-rendering
function ChatFeed({ messages }) {
  return (
    <div>
      {messages.map(msg => (
        <ChatMessage
          key={msg.id}
          author={msg.author}
          text={msg.text}
          timestamp={msg.timestamp}
        />
      ))}
    </div>
  );
}

Note React.memo only does a shallow prop comparison. If you pass objects or functions that are recreated each render, memo has no effect. Pair with useMemo/useCallback for those props.

Code Splitting by Route

syntax
const Page = lazy(() => import('./Page'));
<Suspense fallback={<Spinner />}>
  <Page />
</Suspense>
example
import { lazy, Suspense } from 'react';

const Home = lazy(() => import('./pages/Home'));
const Products = lazy(() => import('./pages/Products'));
const Account = lazy(() => import('./pages/Account'));

function AppRouter() {
  const [route, setRoute] = useState('home');

  return (
    <nav>
      <button onClick={() => setRoute('home')}>Home</button>
      <button onClick={() => setRoute('products')}>Products</button>
      <button onClick={() => setRoute('account')}>Account</button>

      <Suspense fallback={<div className="page-loader">Loading page...</div>}>
        {route === 'home' && <Home />}
        {route === 'products' && <Products />}
        {route === 'account' && <Account />}
      </Suspense>
    </nav>
  );
}

Note Route-based code splitting gives the biggest bundle size reduction because entire page components and their dependencies are loaded on demand. Place the Suspense boundary around the lazy components, not the entire app.

Avoiding Unnecessary Renders

syntax
// 1. Move state down (closer to where it is used)
// 2. Lift content up (pass components as children)
// 3. Memoize with React.memo + useCallback/useMemo
example
// PROBLEM: typing in input re-renders the expensive list
function BadPage() {
  const [query, setQuery] = useState('');
  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <ExpensiveList /> {/* re-renders on every keystroke */}
    </div>
  );
}

// FIX 1: Move state down into its own component
function GoodPage() {
  return (
    <div>
      <SearchInput /> {/* state lives here now */}
      <ExpensiveList /> {/* no longer re-renders on keystrokes */}
    </div>
  );
}

function SearchInput() {
  const [query, setQuery] = useState('');
  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

// FIX 2: Lift content up via children
function InputWrapper({ children }) {
  const [query, setQuery] = useState('');
  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      {children} {/* children don't re-render */}
    </div>
  );
}

function Page() {
  return (
    <InputWrapper>
      <ExpensiveList />
    </InputWrapper>
  );
}

Note Before reaching for React.memo, try restructuring your component tree. Moving state down and lifting content up are free optimizations that often eliminate the problem entirely.

Key Prop for Efficient Updates

syntax
// Stable keys help React identify which items changed
<Item key={item.id} />

// Reset component state by changing the key
<Form key={selectedUserId} />
example
// Using key to reset component state
function UserEditor({ userId }) {
  // When userId changes, React destroys the old Form
  // and mounts a fresh one with clean state
  return <ProfileForm key={userId} userId={userId} />;
}

function ProfileForm({ userId }) {
  const [name, setName] = useState('');
  const [bio, setBio] = useState('');

  useEffect(() => {
    fetchUser(userId).then(user => {
      setName(user.name);
      setBio(user.bio);
    });
  }, [userId]);

  return (
    <form>
      <input value={name} onChange={e => setName(e.target.value)} />
      <textarea value={bio} onChange={e => setBio(e.target.value)} />
    </form>
  );
}

Note Changing a component's key forces React to unmount and remount it, resetting all internal state. This is a clean way to 'reset' a form or component when switching between items without manually clearing each state variable.

React Profiler

syntax
<Profiler id="name" onRender={callback}>
  <Component />
</Profiler>
// callback(id, phase, actualDuration, baseDuration, startTime, commitTime)
example
import { Profiler } from 'react';

function onRenderCallback(
  id,
  phase,        // 'mount' or 'update'
  actualDuration, // time spent rendering
  baseDuration,   // estimated time without memoization
  startTime,
  commitTime
) {
  console.log(`${id} [${phase}]: ${actualDuration.toFixed(1)}ms`);
}

function App() {
  return (
    <Profiler id="ProductList" onRender={onRenderCallback}>
      <ProductList />
    </Profiler>
  );
}

Note Use the Profiler component or React DevTools Profiler tab to identify slow renders. Focus on components with high actualDuration. The baseDuration shows the worst-case cost without any memoization.

List Virtualization

syntax
// Render only visible items in long lists
// Use react-window or react-virtuoso
example
// Concept: only render items in the viewport
import { FixedSizeList } from 'react-window';

function VirtualizedUserList({ users }) {
  const Row = ({ index, style }) => (
    <div style={style} className="user-row">
      <span>{users[index].name}</span>
      <span>{users[index].email}</span>
    </div>
  );

  return (
    <FixedSizeList
      height={500}
      itemCount={users.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

// Renders ~10-15 visible rows instead of 10,000

Note For lists with hundreds or thousands of items, virtualization dramatically improves performance by only rendering visible rows. react-window is lightweight; react-virtuoso supports dynamic heights. Measure first -- small lists do not need this.

Common Mistakes

Stale Closures

syntax
// PROBLEM: handler captures old state value
// FIX: use updater function or ref
example
function BrokenCounter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      // BUG: count is always 0 (captured from initial render)
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []); // empty deps = closure captures initial count

  return <p>{count}</p>; // stuck at 1
}

function FixedCounter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      // FIX: updater function always has the latest value
      setCount(prev => prev + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <p>{count}</p>; // increments correctly
}

Note Stale closures happen when a callback captures a state value that never updates. Common in setInterval, setTimeout, and event listeners set up in useEffect. Use the updater form of setState or store values in a ref.

Direct State Mutation

syntax
// BAD: mutating state directly
state.items.push(newItem);
setState(state);

// GOOD: create a new reference
setState({ ...state, items: [...state.items, newItem] });
example
function BrokenTags() {
  const [tags, setTags] = useState(['react', 'hooks']);

  function addTag(tag) {
    // BUG: push mutates the existing array
    tags.push(tag);
    setTags(tags); // same reference, React sees no change
  }

  return (
    <div>
      {tags.map(t => <span key={t}>{t}</span>)}
      <button onClick={() => addTag('new')}>Add</button>
    </div>
  );
}

function FixedTags() {
  const [tags, setTags] = useState(['react', 'hooks']);

  function addTag(tag) {
    // CORRECT: spread creates a new array
    setTags(prev => [...prev, tag]);
  }

  return (
    <div>
      {tags.map(t => <span key={t}>{t}</span>)}
      <button onClick={() => addTag('new')}>Add</button>
    </div>
  );
}

Note React uses reference equality (Object.is) to decide whether to re-render. Mutating an object or array in place keeps the same reference, so React skips the update. Always produce a new object/array via spread, map, filter, or slice.

Missing or Bad Keys

syntax
// BAD: index as key with reorderable list
{items.map((item, i) => <Item key={i} />)}

// GOOD: stable unique identifier
{items.map(item => <Item key={item.id} />)}
example
// BUG: using index as key with a removable list
function BrokenList() {
  const [items, setItems] = useState([
    { id: 'a', text: 'First' },
    { id: 'b', text: 'Second' },
    { id: 'c', text: 'Third' },
  ]);

  function removeFirst() {
    setItems(prev => prev.slice(1));
  }

  return (
    <div>
      {/* Using index: after removing first item,
         React thinks items shifted, inputs lose state */}
      {items.map((item, index) => (
        <div key={index}>
          <input defaultValue={item.text} />
        </div>
      ))}
      <button onClick={removeFirst}>Remove First</button>
    </div>
  );
}

// FIX: use item.id as key
// {items.map(item => <div key={item.id}>...)}

Note Index keys cause broken behavior when items are added, removed, or reordered. React associates state (including uncontrolled input values) with the key. When keys shift, state gets attached to the wrong item.

useEffect Dependency Issues

syntax
// MISSING DEPS: effect uses stale values
useEffect(() => { fn(a, b); }, []); // a, b missing

// OBJECT DEPS: infinite loop
useEffect(() => { ... }, [{ x: 1 }]); // new ref each render
example
// BUG: missing dependency
function Greeting({ name }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    // name is used but not in deps -- stale value
    setMessage(`Hello, ${name}!`);
  }, []); // should be [name]

  return <p>{message}</p>;
}

// BUG: object dependency causes infinite loop
function DataLoader({ filters }) {
  useEffect(() => {
    fetchData(filters);
  }, [filters]); // if parent recreates filters object each render, infinite loop!

  // FIX: destructure primitive values
  const { category, minPrice } = filters;
  useEffect(() => {
    fetchData({ category, minPrice });
  }, [category, minPrice]);
}

Note Enable the eslint-plugin-react-hooks exhaustive-deps rule to catch missing dependencies. For object dependencies, extract primitive values or memoize the object in the parent. Never suppress the lint rule without understanding why.

Infinite Re-renders

syntax
// CAUSE 1: setState during render
function Bad() {
  const [x, setX] = useState(0);
  setX(1); // triggers re-render during render!
}

// CAUSE 2: object/array in useEffect deps
useEffect(() => { ... }, [{ a: 1 }]); // new ref every render
example
// BUG: calling setState unconditionally during render
function BrokenComponent({ data }) {
  const [processed, setProcessed] = useState(null);
  // This runs on every render, which triggers a new render...
  setProcessed(transform(data));
  return <Display data={processed} />;
}

// FIX: derive the value without state
function FixedComponent({ data }) {
  const processed = transform(data); // compute inline
  return <Display data={processed} />;
}

// BUG: onClick={handler()} calls immediately
function BrokenButton() {
  const [count, setCount] = useState(0);
  // handler() executes during render, calling setCount, looping
  return <button onClick={setCount(count + 1)}>Click</button>;
}

// FIX: pass function reference
function FixedButton() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Click</button>;
}

Note Infinite re-render loops crash your app. The most common causes: (1) calling setState directly in the render body, (2) onClick={fn()} instead of onClick={fn}, (3) useEffect with an object/array literal in the dependency array that creates a new reference every render.

Overusing useEffect

syntax
// ANTI-PATTERN: effect to sync derived state
useEffect(() => { setB(computeFrom(a)); }, [a]);

// BETTER: compute inline
const b = computeFrom(a);
example
// BAD: useEffect chain
function PriceCalculator({ items }) {
  const [subtotal, setSubtotal] = useState(0);
  const [tax, setTax] = useState(0);
  const [total, setTotal] = useState(0);

  useEffect(() => {
    setSubtotal(items.reduce((s, i) => s + i.price, 0));
  }, [items]);

  useEffect(() => {
    setTax(subtotal * 0.1);
  }, [subtotal]);

  useEffect(() => {
    setTotal(subtotal + tax);
  }, [subtotal, tax]);
  // 3 extra renders for what should be 0!

  return <p>Total: ${total.toFixed(2)}</p>;
}

// GOOD: calculate everything inline
function PriceCalculator({ items }) {
  const subtotal = items.reduce((s, i) => s + i.price, 0);
  const tax = subtotal * 0.1;
  const total = subtotal + tax;

  return <p>Total: ${total.toFixed(2)}</p>;
}

Note Each unnecessary useEffect + setState causes an extra render cycle. If a value can be derived from props or state, compute it during render. Reserve useEffect for genuine side effects: API calls, subscriptions, DOM manipulation, and third-party library integration.

Setting State After Unmount

syntax
// PROBLEM: async callback sets state on unmounted component
useEffect(() => {
  fetchData().then(data => setState(data)); // may be unmounted!
}, []);

// FIX: track mounted status
useEffect(() => {
  let active = true;
  fetchData().then(data => { if (active) setState(data); });
  return () => { active = false; };
}, []);
example
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

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

    async function loadUser() {
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();

      // Only update state if this effect is still active
      if (!cancelled) {
        setUser(data);
      }
    }

    loadUser();

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

  if (!user) return <p>Loading...</p>;
  return <h1>{user.name}</h1>;
}

Note While React 18+ no longer warns about this, it is still a logic bug. Without the cancelled flag, a slow request for user A could resolve after switching to user B, overwriting user B's data with user A's data. Always use a cleanup flag or AbortController.

State Batching Behavior

syntax
// React 18+ batches ALL state updates automatically
function handleClick() {
  setA(1);  // does not trigger render
  setB(2);  // does not trigger render
  setC(3);  // single render with all three updates
}
example
function StatusPanel() {
  const [loading, setLoading] = useState(false);
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  async function loadData() {
    setLoading(true);
    setError(null);
    // React batches these two -- one render

    try {
      const result = await fetchDashboardData();
      setData(result);
      setLoading(false);
      // React batches these two -- one render
    } catch (err) {
      setError(err.message);
      setLoading(false);
      // React batches these two -- one render
    }
  }

  return (
    <div>
      {loading && <p>Loading...</p>}
      {error && <p>Error: {error}</p>}
      {data && <Dashboard data={data} />}
    </div>
  );
}

Note Since React 18, automatic batching applies to all updates, including those in promises, setTimeout, and native event handlers. You rarely need to worry about batching anymore. If you ever need to force a synchronous render, use flushSync (imported from react-dom) as a last resort.