Back to Home
    ReactReact v19Intermediate

    React

    React hooks, component patterns, state management, performance optimisation, and best practices.

    12 min read
    reacthooksfrontendui

    Core Hooks

    3 topics

    useState & useReducer

    tsx
    import { useState, useReducer } from "react";
    
    // useState
    const [count, setCount] = useState(0);
    const [user, setUser] = useState<User | null>(null);
    
    // Functional update (safe for async)
    setCount(prev => prev + 1);
    
    // useReducer for complex state
    type Action = { type: "inc" } | { type: "dec" } | { type: "reset" };
    
    function reducer(state: number, action: Action): number {
      switch (action.type) {
        case "inc": return state + 1;
        case "dec": return state - 1;
        case "reset": return 0;
      }
    }
    
    const [count, dispatch] = useReducer(reducer, 0);
    dispatch({ type: "inc" });

    💡 Use functional updates when new state depends on old state

    ⚡ useReducer shines when state has multiple sub-values or complex transitions

    useEffect

    tsx
    import { useEffect, useState } from "react";
    
    // Run after every render
    useEffect(() => { document.title = `Count: ${count}`; });
    
    // Run once on mount
    useEffect(() => {
      fetchUser(id).then(setUser);
    }, []);
    
    // Run when deps change
    useEffect(() => {
      const sub = subscribe(userId);
      return () => sub.unsubscribe(); // Cleanup!
    }, [userId]);
    
    // ✅ Better: use async function inside
    useEffect(() => {
      async function load() {
        const data = await fetchUser(id);
        setUser(data);
      }
      load();
    }, [id]);

    ⚠️ Always return a cleanup function for subscriptions/timers

    💡 Never make the useEffect callback async — define async inside it

    useMemo & useCallback

    tsx
    import { useMemo, useCallback } from "react";
    
    // useMemo: memoize expensive computation
    const sorted = useMemo(
      () => [...items].sort((a, b) => a.name.localeCompare(b.name)),
      [items]
    );
    
    // useCallback: stable function reference
    const handleClick = useCallback(
      (id: number) => dispatch({ type: "remove", id }),
      [dispatch]
    );
    
    // ✅ Use when passing to memoized child
    const Child = React.memo(({ onClick }) => (
      <button onClick={onClick}>Click</button>
    ));

    💡 Only memoize when you have a measured performance problem

    ⚠️ Over-memoizing adds overhead — profile first

    Core Components and Props

    3 topics

    Create Reusable Function Components

    Build UI pieces as small, reusable function components. Pass configuration through props so components stay flexible and testable.

    jsx
    function Badge({ label, tone = 'info' }) {
      return <span className={`badge badge--${tone}`}>{label}</span>;
    }
    
    export default function UserCard({ user }) {
      return (
        <article className="card">
          <h3>{user.name}</h3>
          <p>{user.email}</p>
          <Badge label={user.role} tone={user.role === 'Admin' ? 'success' : 'info'} />
        </article>
      );
    }

    Use default values in destructuring for optional props.

    Keep components focused on one UI responsibility.

    componentspropsreusability

    Render Lists with Stable Keys

    When rendering arrays, always use stable unique keys (like IDs) to avoid UI glitches and unnecessary re-renders.

    jsx
    function TodoList({ todos, onToggle }) {
      return (
        <ul>
          {todos.map((todo) => (
            <li key={todo.id}>
              <label>
                <input
                  type="checkbox"
                  checked={todo.done}
                  onChange={() => onToggle(todo.id)}
                />
                {todo.title}
              </label>
            </li>
          ))}
        </ul>
      );
    }

    Never use array index as key for dynamic/reorderable lists.

    Keys should come from your data model.

    listskeysrendering

    Conditional UI for Loading and Empty States

    Show explicit loading, empty, and error states so users understand what is happening in data-driven views.

    jsx
    function OrdersPanel({ isLoading, error, orders }) {
      if (isLoading) return <p>Loading orders…</p>;
      if (error) return <p role="alert">Failed to load orders.</p>;
      if (orders.length === 0) return <p>No orders yet.</p>;
    
      return (
        <ul>
          {orders.map((o) => (
            <li key={o.id}>#{o.id}{o.status}</li>
          ))}
        </ul>
      );
    }

    Return early for cleaner render logic.

    Always include empty-state UX in dashboards and tables.

    conditional-renderingux

    Component Patterns

    2 topics

    Custom Hooks

    tsx
    import { useState, useEffect } from "react";
    
    function useFetch<T>(url: string) {
      const [data, setData] = useState<T | null>(null);
      const [loading, setLoading] = useState(true);
      const [error, setError] = useState<Error | null>(null);
    
      useEffect(() => {
        let cancelled = false;
        setLoading(true);
        fetch(url)
          .then(r => r.json())
          .then(d => { if (!cancelled) setData(d); })
          .catch(e => { if (!cancelled) setError(e); })
          .finally(() => { if (!cancelled) setLoading(false); });
        return () => { cancelled = true; };
      }, [url]);
    
      return { data, loading, error };
    }
    
    // Usage
    const { data: user, loading } = useFetch<User>(`/api/users/${id}`);

    💡 Extract reusable logic into custom hooks prefixed with 'use'

    ⚡ The cancelled flag prevents state updates on unmounted components

    Context API

    tsx
    import { createContext, useContext, useState } from "react";
    
    interface ThemeCtx { theme: string; toggle: () => void; }
    const ThemeContext = createContext<ThemeCtx | null>(null);
    
    export function ThemeProvider({ children }: { children: React.ReactNode }) {
      const [theme, setTheme] = useState("light");
      const toggle = () => setTheme(t => t === "light" ? "dark" : "light");
      return (
        <ThemeContext.Provider value={{ theme, toggle }}>
          {children}
        </ThemeContext.Provider>
      );
    }
    
    export function useTheme() {
      const ctx = useContext(ThemeContext);
      if (!ctx) throw new Error("useTheme must be inside ThemeProvider");
      return ctx;
    }

    💡 Throw in the hook if context is null — catches missing Provider early

    ⚡ Split contexts by update frequency to avoid unnecessary re-renders

    State and Hooks

    3 topics

    Manage Local State with useState

    Use useState for component-local data like toggles, inputs, and simple objects.

    jsx
    import { useState } from 'react';
    
    export default function QuantitySelector() {
      const [qty, setQty] = useState(1);
    
      return (
        <div>
          <button onClick={() => setQty((q) => Math.max(1, q - 1))}>-</button>
          <span>{qty}</span>
          <button onClick={() => setQty((q) => q + 1)}>+</button>
        </div>
      );
    }

    Use functional updates when next state depends on previous state.

    Keep state minimal; derive values when possible.

    useStatestate

    Handle Side Effects with useEffect

    Use effects for syncing with external systems (APIs, timers, subscriptions), not for pure calculations.

    jsx
    import { useEffect, useState } from 'react';
    
    export default function ProductList() {
      const [products, setProducts] = useState([]);
      const [loading, setLoading] = useState(true);
    
      useEffect(() => {
        let ignore = false;
    
        async function load() {
          setLoading(true);
          const res = await fetch('/api/products');
          const data = await res.json();
          if (!ignore) setProducts(data);
          if (!ignore) setLoading(false);
        }
    
        load();
        return () => { ignore = true; };
      }, []);
    
      if (loading) return <p>Loading…</p>;
      return <pre>{JSON.stringify(products, null, 2)}</pre>;
    }

    Add cleanup logic to avoid setting state on unmounted components.

    Prefer framework/data-layer fetching when available.

    useEffectdata-fetching

    Share Logic with Custom Hooks

    Extract repeated stateful logic into custom hooks to keep components lean and consistent across pages.

    jsx
    import { useEffect, useState } from 'react';
    
    function useOnlineStatus() {
      const [isOnline, setIsOnline] = useState(navigator.onLine);
    
      useEffect(() => {
        const onOnline = () => setIsOnline(true);
        const onOffline = () => setIsOnline(false);
        window.addEventListener('online', onOnline);
        window.addEventListener('offline', onOffline);
        return () => {
          window.removeEventListener('online', onOnline);
          window.removeEventListener('offline', onOffline);
        };
      }, []);
    
      return isOnline;
    }
    
    export default function StatusBanner() {
      const isOnline = useOnlineStatus();
      return <p>{isOnline ? '🟢 Online' : '🔴 Offline'}</p>;
    }

    Prefix custom hooks with use.

    Custom hooks can return values, functions, or objects.

    custom-hooksreusability

    Forms and User Input

    2 topics

    Controlled Form Inputs

    Use controlled components for predictable input handling, validation, and submission.

    jsx
    import { useState } from 'react';
    
    export default function SignupForm() {
      const [form, setForm] = useState({ email: '', password: '' });
    
      function onChange(e) {
        const { name, value } = e.target;
        setForm((f) => ({ ...f, [name]: value }));
      }
    
      function onSubmit(e) {
        e.preventDefault();
        console.log('submit', form);
      }
    
      return (
        <form onSubmit={onSubmit}>
          <input name="email" value={form.email} onChange={onChange} placeholder="Email" />
          <input name="password" type="password" value={form.password} onChange={onChange} placeholder="Password" />
          <button type="submit">Create account</button>
        </form>
      );
    }

    Use one change handler for multiple fields via name attribute.

    Prevent default form submission in SPA flows.

    formscontrolled-components

    Client-Side Validation Before Submit

    Validate required fields and constraints before API calls to reduce failed requests and improve UX.

    jsx
    import { useState } from 'react';
    
    export default function ProfileForm() {
      const [name, setName] = useState('');
      const [error, setError] = useState('');
    
      function handleSubmit(e) {
        e.preventDefault();
        if (name.trim().length < 2) {
          setError('Name must be at least 2 characters');
          return;
        }
        setError('');
        // await fetch('/api/profile', { method: 'POST', body: JSON.stringify({ name }) })
      }
    
      return (
        <form onSubmit={handleSubmit}>
          <input value={name} onChange={(e) => setName(e.target.value)} />
          {error && <p role="alert">{error}</p>}
          <button>Save</button>
        </form>
      );
    }

    Show field-specific messages near the input.

    Combine client validation with server-side validation.

    validationux

    Routing and App Structure

    2 topics

    Set Up Routes with React Router

    Organize SPA pages with nested routes, layouts, and parameterized paths.

    jsx
    import { BrowserRouter, Routes, Route, Link, useParams } from 'react-router-dom';
    
    function Layout() {
      return (
        <>
          <nav>
            <Link to="/">Home</Link> | <Link to="/products/42">Product</Link>
          </nav>
          <hr />
        </>
      );
    }
    
    function ProductPage() {
      const { productId } = useParams();
      return <h2>Product #{productId}</h2>;
    }
    
    export default function App() {
      return (
        <BrowserRouter>
          <Layout />
          <Routes>
            <Route path="/" element={<h1>Home</h1>} />
            <Route path="/products/:productId" element={<ProductPage />} />
          </Routes>
        </BrowserRouter>
      );
    }

    Use route params for entity detail pages.

    Keep persistent nav and shell in layout components.

    routingreact-router

    Feature-Based Folder Structure

    Group files by feature (orders, auth, dashboard) rather than file type to improve scalability in teams.

    src/
      app/
        router.jsx
        providers.jsx
      features/
        auth/
          api.js
          AuthPage.jsx
          authSlice.js
        orders/
          api.js
          OrdersPage.jsx
          OrderRow.jsx
          useOrders.js
      shared/
        components/
          Button.jsx
          Modal.jsx
        utils/
          formatCurrency.js

    Start simple and refactor into features as app grows.

    Keep shared modules truly generic to avoid coupling.

    architecturescalability

    Performance and Production Patterns

    3 topics

    Prevent Expensive Recomputations with useMemo

    Memoize heavy calculations so they only rerun when dependencies change.

    jsx
    import { useMemo, useState } from 'react';
    
    function filterProducts(products, query) {
      console.log('Filtering...');
      return products.filter((p) => p.name.toLowerCase().includes(query.toLowerCase()));
    }
    
    export default function SearchableProducts({ products }) {
      const [query, setQuery] = useState('');
      const filtered = useMemo(() => filterProducts(products, query), [products, query]);
    
      return (
        <>
          <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search" />
          <p>{filtered.length} results</p>
        </>
      );
    }

    Use useMemo only for measurable expensive work.

    Avoid premature optimization.

    performanceuseMemo

    Split Code with React.lazy and Suspense

    Load large pages on demand to reduce initial bundle size and improve first load performance.

    jsx
    import { Suspense, lazy } from 'react';
    import { Routes, Route } from 'react-router-dom';
    
    const ReportsPage = lazy(() => import('./ReportsPage'));
    
    export default function AppRoutes() {
      return (
        <Suspense fallback={<p>Loading page…</p>}>
          <Routes>
            <Route path="/reports" element={<ReportsPage />} />
          </Routes>
        </Suspense>
      );
    }

    Lazy-load route-level components first for biggest impact.

    Use meaningful fallback UI (skeletons/spinners).

    code-splittingsuspense

    Error Boundaries for Safer Production UIs

    Catch rendering errors in subtree components and show fallback UI instead of a full app crash.

    jsx
    import React from 'react';
    
    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props);
        this.state = { hasError: false };
      }
    
      static getDerivedStateFromError() {
        return { hasError: true };
      }
    
      componentDidCatch(error, info) {
        console.error('UI error:', error, info);
        // Send to Sentry/Datadog/etc.
      }
    
      render() {
        if (this.state.hasError) return <h2>Something went wrong.</h2>;
        return this.props.children;
      }
    }
    
    export default ErrorBoundary;

    Wrap unstable or third-party-heavy sections with boundaries.

    Log errors to monitoring tools for triage.

    error-handlingproduction

    Related Articles

    Background reading and deeper explanations for this sheet.

    Intro to FastAPI: Build a Production-Ready Python API with Real Examples

    FastAPI is one of the fastest ways to build modern APIs in Python without sacrificing code quality or developer experience. In this practical, beginner-friendly guide, you’ll learn how to create endpoints, validate data, handle errors, connect a database, secure routes, and prepare your FastAPI app for real-world deployment.

    keyword overlap

    GenAI SaaS Architecture: A Practical Blueprint for Building, Scaling, and Securing AI Products

    Designing a SaaS product on top of GenAI is more than calling an LLM API—it requires thoughtful architecture across product, data, safety, and operations. This practical guide walks you through a beginner-friendly yet deep system blueprint, with real-world patterns, code snippets, and deployment strategies you can use immediately.

    keyword overlap

    Scaling AI APIs with FastAPI, Docker, and Load Balancers: A Practical Guide for Production

    Building an AI API is easy—keeping it fast and reliable under real traffic is the hard part. In this practical guide, you’ll learn how to scale FastAPI services with Docker and load balancers, from local setup to production architecture, with real-world examples, code, and deployment tips.

    keyword overlap

    Event-Driven Agents with Kafka Streams: A Practical Guide for Building Real-Time AI Workflows

    Event-driven agents let you move from slow, request/response automation to responsive systems that react in milliseconds to real-world signals. In this hands-on guide, you’ll learn how to design, build, and operate agentic workflows using Apache Kafka and Kafka Streams, with practical patterns, code snippets, and production-ready advice.

    keyword overlap