Skip to content

React Reference

React Reference

React 18/19 patterns you use every day: component patterns, state, performance, data fetching, Server Components, and the hooks that replace most class boilerplate.

Component patterns — function, memo, forwardRef
// Function component — the standard
function Button({ label, onClick, disabled = false, variant = "primary" }) {
  return (
    
  );
}

// TypeScript props
interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
  variant?: "primary" | "secondary" | "danger";
  children?: React.ReactNode;
}

// React.memo — skip re-render when props haven't changed
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
  return 
    {items.map(i =>
  • {i.name}
  • )}
; }); // Custom comparison function (when shallow equality isn't enough) const Chart = React.memo(Chart, (prev, next) => prev.data === next.data && prev.height === next.height ); // forwardRef — expose DOM node to parent const Input = React.forwardRef(function Input({ label, ...props }, ref) { return ( ); }); // useImperativeHandle — expose custom methods (not just DOM node) const Dialog = React.forwardRef(function Dialog({ children }, ref) { const dialogRef = useRef(); useImperativeHandle(ref, () => ({ open: () => dialogRef.current.showModal(), close: () => dialogRef.current.close(), })); return {children}; }); // Compound components pattern const Tabs = Object.assign(TabsRoot, { Tab, Panel, List });
State patterns — useState, useReducer, lifting state
// useState — simple values
const [count, setCount] = useState(0);
const [user, setUser]   = useState(null);

// Functional update — always use when new state depends on old
setCount(c => c + 1);             // safe even in StrictMode / batching
setItems(prev => [...prev, item]);

// useState with lazy initialisation (expensive initial value)
const [state] = useState(() => JSON.parse(localStorage.getItem("state") || "{}"));

// useReducer — for complex state transitions
type Action =
  | { type: "increment" }
  | { type: "decrement" }
  | { type: "reset"; payload: number };

function reducer(state: number, action: Action): number {
  switch (action.type) {
    case "increment": return state + 1;
    case "decrement": return state - 1;
    case "reset":     return action.payload;
    default:          return state;
  }
}

const [count, dispatch] = useReducer(reducer, 0);
dispatch({ type: "reset", payload: 10 });

// Lifting state — move shared state to closest common ancestor
// BAD: two siblings sharing state via props drilling
// GOOD: lift state to parent, pass down via props or Context

// Object state — spread to update one field
const [form, setForm] = useState({ name: "", email: "", role: "user" });
const updateField = (field: string) => (e: React.ChangeEvent) =>
  setForm(prev => ({ ...prev, [field]: e.target.value }));

// Batching (React 18+) — multiple setStates in event handlers
// and async code are batched automatically
async function handleClick() {
  setLoading(true);
  const data = await fetch("/api");      // batched with above
  setData(data);
  setLoading(false);
}
useEffect — data fetching, subscriptions, cleanup
// Basic effect
useEffect(() => {
  document.title = `${count} items`;
}, [count]);   // only re-run when count changes

// Empty array = run once after mount
useEffect(() => {
  const id = analytics.init();
  return () => analytics.cleanup(id);   // cleanup on unmount
}, []);

// No array = run after EVERY render (almost never what you want)

// Data fetching in useEffect (old pattern — prefer React Query or use)
useEffect(() => {
  let cancelled = false;   // handle stale closures

  async function load() {
    setLoading(true);
    try {
      const data = await fetchUser(userId);
      if (!cancelled) setUser(data);
    } catch (err) {
      if (!cancelled) setError(err);
    } finally {
      if (!cancelled) setLoading(false);
    }
  }

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

// AbortController — cancel fetch on cleanup
useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/users/${id}`, { signal: controller.signal })
    .then(r => r.json())
    .then(setUser)
    .catch(err => { if (err.name !== "AbortError") setError(err); });

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

// Subscription pattern
useEffect(() => {
  const unsub = store.subscribe(() => setState(store.getState()));
  return unsub;
}, []);

// What goes in the dependency array?
// - Everything from the component scope used inside the effect
// - Omitting deps is almost always a bug
// - use eslint-plugin-react-hooks to catch missing deps

In React 19 / future React, the use() hook + Suspense replaces most useEffect data fetching. Start migrating to React Query, SWR, or use() for new code.

useMemo and useCallback — when to actually use them
// useMemo — cache expensive computed value
const sorted = useMemo(
  () => [...items].sort((a, b) => a.name.localeCompare(b.name)),
  [items]
);

// useCallback — stable function reference
const handleSubmit = useCallback(async (data: FormData) => {
  await api.post("/submit", data);
  onSuccess();
}, [onSuccess]);   // only recreated if onSuccess changes

// When to use them:
// useMemo:     expensive computation (> 1ms), passed as dep to other hooks
// useCallback: stable ref needed for React.memo'd child, or as dep in useEffect

// When NOT to bother:
// - Simple transforms that run in microseconds
// - Values not passed to memoised children
// - "Just in case" — profile first, optimise second

// useTransition — keep UI responsive during heavy updates (React 18+)
const [isPending, startTransition] = useTransition();

function handleInput(e) {
  setInputValue(e.target.value);   // immediate — update input
  startTransition(() => {
    setFilteredResults(filter(allItems, e.target.value));  // deferred
  });
}

// useDeferredValue — like useTransition but for values you don't control
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => search(data, deferredQuery), [data, deferredQuery]);
Context — global state without prop drilling
// Define context with a sensible default and a provider hook
interface ThemeContextValue {
  theme: "light" | "dark";
  toggle: () => void;
}

const ThemeContext = React.createContext(null);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<"light" | "dark">("light");

  const value = useMemo(() => ({
    theme,
    toggle: () => setTheme(t => t === "light" ? "dark" : "light"),
  }), [theme]);

  return (
    
      {children}
    
  );
}

// Custom hook — validates usage and provides type safety
export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error("useTheme must be used inside ThemeProvider");
  return ctx;
}

// Composing multiple providers (common pattern)
function AppProviders({ children }) {
  return (
    
      
        
          {children}
        
      
    
  );
}

// Context + useReducer — lightweight global state
const StateContext = createContext(null);
const DispatchContext = createContext(null);

export function useAppState() { return useContext(StateContext); }
export function useAppDispatch() { return useContext(DispatchContext); }

// Split context avoids re-renders in components that only dispatch
Custom hooks — reusable stateful logic
// useLocalStorage — persist state in localStorage
function useLocalStorage(key: string, initialValue: T) {
  const [value, setValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const set = useCallback((v: T | ((prev: T) => T)) => {
    setValue(prev => {
      const next = typeof v === "function" ? (v as Function)(prev) : v;
      localStorage.setItem(key, JSON.stringify(next));
      return next;
    });
  }, [key]);

  return [value, set] as const;
}

// useDebounce — delay updates (search inputs)
function useDebounce(value: T, delay: number): T {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
  return debounced;
}

// useFetch — data fetching with loading/error state
function useFetch(url: string) {
  const [state, setState] = useState<{
    data: T | null; loading: boolean; error: Error | null;
  }>({ data: null, loading: true, error: null });

  useEffect(() => {
    const controller = new AbortController();
    fetch(url, { signal: controller.signal })
      .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
      .then(data => setState({ data, loading: false, error: null }))
      .catch(err => { if (err.name !== "AbortError") setState(s => ({ ...s, loading: false, error: err })); });
    return () => controller.abort();
  }, [url]);

  return state;
}

// useEventListener — safe event listener management
function useEventListener(
  event: string,
  handler: (e: T) => void,
  target: EventTarget = window
) {
  const savedHandler = useRef(handler);
  useEffect(() => { savedHandler.current = handler; }, [handler]);
  useEffect(() => {
    const fn = (e: Event) => savedHandler.current(e as T);
    target.addEventListener(event, fn);
    return () => target.removeEventListener(event, fn);
  }, [event, target]);
}
Server Components and React 19
// React Server Components (Next.js App Router / React 19)
// Default in App Router — run on server, no client JS

// Server Component — async, can fetch directly
async function UserProfile({ id }: { id: string }) {
  const user = await db.users.findUnique({ where: { id } });  // direct DB access
  if (!user) notFound();

  return (
    

{user.name}

}> {/* another async Server Component */}
); } // Client Component — needs interactivity, browser APIs "use client"; function Counter() { const [count, setCount] = useState(0); return ; } // Pass Server data to Client Component async function Page() { const initialData = await fetchData(); return ; // data serialised to client } // use() hook (React 19) — consume Promises and Context anywhere import { use } from "react"; function UserCard({ userPromise }: { userPromise: Promise }) { const user = use(userPromise); // Suspense boundary handles loading return
{user.name}
; } // Server Actions — mutate data from Client Components "use server"; async function updateUser(formData: FormData) { const name = formData.get("name") as string; await db.users.update({ where: { id: getSession().userId }, data: { name } }); revalidatePath("/profile"); } // Use in form
Performance — profiling and common fixes
// Code splitting — lazy load components
const HeavyChart = lazy(() => import("./HeavyChart"));

function Dashboard() {
  return (
    }>
      
    
  );
}

// Virtualise long lists — only render visible rows
import { FixedSizeList } from "react-window";


  {({ index, style }) => }


// Avoid creating objects/arrays in JSX (causes child re-renders)
// BAD

// GOOD
const STYLE   = { color: "red" };
const OPTIONS = ["a", "b"];


// Key prop — stable, unique identifiers
// BAD: index as key — breaks when list order changes
{items.map((item, i) => )}
// GOOD: stable ID
{items.map(item => )}

// React DevTools Profiler — find re-renders
// Components → gear icon → "Highlight updates when components render"

// Why did this render? (react-scan or Why Did You Render)
// npm install @welldone-software/why-did-you-render
Component.whyDidYouRender = true;

// Measure render performance
import { unstable_trace as trace } from "scheduler/tracing";
trace("click", performance.now(), () => dispatch(action));
Error boundaries and Suspense
// Error Boundary — catch render errors (still needs class component in React 18)
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

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

  componentDidCatch(error, info) {
    reportError(error, info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback ?? 
Something went wrong.
; } return this.props.children; } } // react-error-boundary package (recommended — simpler API) import { ErrorBoundary } from "react-error-boundary"; (

Error: {error.message}

)} onError={(error, info) => logToSentry(error, info)} >
// Suspense — show fallback while async content loads }> // Nested Suspense — granular loading states }>
}> }>

🔍 Free tool: npm Package Health Checker — check React and its ecosystem packages for EOL status, known CVEs, and latest versions before upgrading.

Founded

2023 in London, UK

Contact

hello@releaserun.com