cheat sheet

React Basics

Foundational React patterns — function components, JSX, props, hooks (useState, useEffect, useRef, useContext), list rendering, and form handling — with TypeScript throughout.

React Basics — Function components, hooks, and JSX

What it is

React is a JavaScript library for building user interfaces by composing reusable components. It is developed by Meta and a large open-source community, written in TypeScript, and runs anywhere JavaScript runs — browser, server (via frameworks like Next.js or Remix), and native (via React Native). Modern React is "function components plus hooks": no classes, no lifecycle methods, just plain functions that return JSX and call hooks to read state and side-effects. Alternatives in the same niche include Vue, Svelte, and Solid — but React's ecosystem and hiring market remain the broadest.

Install

There is no react CLI — you scaffold a React project with a build tool. Vite is the most common modern starter; Next.js is the dominant meta-framework. For an existing project, install react and react-dom together.

bash
# Brand new Vite + React + TypeScript project
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev

# Add React to an existing project
npm install react react-dom
npm install -D @types/react @types/react-dom

Output: (none — exits 0 on success)

Hello, function component

A function component is a JavaScript function that returns a piece of UI as JSX. It receives data through props (its single argument) and renders whatever JSX the function evaluates to. The component name must start with a capital letter — that is how the JSX compiler tells <Hello /> (a component) apart from <div> (a DOM tag).

tsx
// src/components/Hello.tsx
type HelloProps = { name: string };

export function Hello({ name }: HelloProps) {
  return <h1>Hello, {name}!</h1>;
}

// src/main.tsx — mount it
import { createRoot } from "react-dom/client";
import { Hello } from "./components/Hello";

const root = createRoot(document.getElementById("root")!);
root.render(<Hello name="Alice Dev" />);

Output: (rendered to the DOM)

text
<h1>Hello, Alice Dev!</h1>

JSX

JSX is an HTML-like syntax that compiles to React.createElement(...) calls. Anything inside {} is a JavaScript expression that is evaluated and inserted. Attributes follow JS naming (className, not class; htmlFor, not for; onClick, not onclick). A component must return a single root node — wrap multiple siblings in a <>...</> fragment if you do not want an extra DOM element.

tsx
function Greeting({ user }: { user: { name: string; admin: boolean } }) {
  const now = new Date().toLocaleTimeString();

  return (
    <>
      <h2 className="title">Hello, {user.name}</h2>
      <p>Time: {now}</p>
      {user.admin && <span className="badge">admin</span>}
      <ul style={{ listStyle: "square" }}>
        {[1, 2, 3].map((n) => (
          <li key={n}>Item {n}</li>
        ))}
      </ul>
    </>
  );
}

Output: (rendered HTML)

text
<h2 class="title">Hello, Alice Dev</h2>
<p>Time: 09:42:13</p>
<span class="badge">admin</span>
<ul style="list-style: square;">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>

Props and children

Props are the read-only inputs to a component, passed as JSX attributes. The special children prop receives whatever JSX is nested between the component's opening and closing tags — this is how layout/wrapper components work. Type props with a TS interface or type alias for safety and editor autocomplete.

tsx
import type { ReactNode } from "react";

type CardProps = {
  title: string;
  variant?: "default" | "warning" | "danger";
  children: ReactNode;
};

export function Card({ title, variant = "default", children }: CardProps) {
  return (
    <section className={`card card-${variant}`}>
      <header>{title}</header>
      <div className="card-body">{children}</div>
    </section>
  );
}

// Usage
function App() {
  return (
    <Card title="Account" variant="warning">
      <p>Your trial ends in 3 days.</p>
      <button>Upgrade</button>
    </Card>
  );
}

Output: (rendered HTML)

text
<section class="card card-warning">
  <header>Account</header>
  <div class="card-body">
    <p>Your trial ends in 3 days.</p>
    <button>Upgrade</button>
  </div>
</section>

Common prop types

TypePurpose
string, number, booleanPrimitive props
ReactNodeAnything renderable (string, number, element, array, null)
ReactElementA single JSX element
(e: React.MouseEvent) => voidClick handlers
(e: React.ChangeEvent<HTMLInputElement>) => voidInput change handlers
React.CSSPropertiesInline style objects
React.ComponentProps<"button">All native props of a tag

useState

useState adds local mutable state to a function component. It returns a [value, setter] tuple: read value to render, call setter(next) to schedule a re-render with the new value. The setter is batched and asynchronous — calling it does not change the current value variable inside the same function call. To compute the next value from the previous, pass a function: setCount(c => c + 1).

tsx
import { useState } from "react";

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

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount((c) => c + 1)}>+1 (functional)</button>
      <button onClick={() => setCount(0)}>reset</button>
    </div>
  );
}

Output: (after clicking +1 three times)

text
Count: 3

Lazy initial state

If the initial value is expensive to compute, pass a function instead of a value. React will only call it on the first render.

tsx
const [users, setUsers] = useState<User[]>(() => {
  const cached = localStorage.getItem("users");
  return cached ? JSON.parse(cached) : [];
});

Output: (none — exits 0 on success)

State with objects and arrays

State must be treated as immutable — never mutate it, always create a new object/array. React compares state with Object.is, so a mutation goes unnoticed and no re-render happens.

tsx
const [user, setUser] = useState({ name: "Alice Dev", age: 30 });

// WRONG — mutation, no re-render
// user.age = 31;

// RIGHT — new object
setUser({ ...user, age: 31 });
setUser((prev) => ({ ...prev, age: prev.age + 1 }));

const [todos, setTodos] = useState<string[]>([]);
setTodos((prev) => [...prev, "buy milk"]);          // append
setTodos((prev) => prev.filter((t) => t !== "buy milk")); // remove

Output: (none — exits 0 on success)

useEffect

useEffect runs side effects (network requests, subscriptions, DOM mutations, timers) after the component renders. The second argument is a dependency array: the effect re-runs only when one of those values changes. Return a cleanup function from the effect to undo subscriptions/timers when the dependencies change or the component unmounts.

tsx
import { useState, useEffect } from "react";

export function Clock() {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    const id = setInterval(() => setTime(new Date()), 1000);
    return () => clearInterval(id);     // cleanup
  }, []);                                // empty deps → run once on mount

  return <p>Current time: {time.toLocaleTimeString()}</p>;
}

Output: (rendered, updates every second)

text
Current time: 09:42:13
Current time: 09:42:14
Current time: 09:42:15

Dependency array rules

PatternWhen effect runs
useEffect(fn)After every render (rare — usually a bug)
useEffect(fn, [])Once after mount; cleanup on unmount
useEffect(fn, [a, b])After mount + whenever a or b changes

Fetching data

tsx
import { useState, useEffect } from "react";

type User = { id: number; name: string };

export function UserCard({ id }: { id: number }) {
  const [user, setUser] = useState<User | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const ctrl = new AbortController();

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

    return () => ctrl.abort();        // cancel on unmount / id change
  }, [id]);

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

Output: (rendered)

text
Loading...
Alice Dev

useRef

useRef returns a mutable container { current: T } that persists across renders without triggering a re-render when you change .current. Two main uses: holding a DOM node reference (pass to a JSX ref attribute) and storing a mutable value that should not cause re-renders (timer IDs, previous values, scroll positions).

tsx
import { useRef, useEffect } from "react";

export function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} placeholder="auto-focused" />;
}

export function StopwatchToggle() {
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);

  const start = () => {
    if (intervalRef.current) return;
    intervalRef.current = setInterval(() => console.log("tick"), 1000);
  };

  const stop = () => {
    if (!intervalRef.current) return;
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  };

  return (
    <>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </>
  );
}

Output: (console after starting and waiting 3 seconds)

text
tick
tick
tick

useContext + createContext

Context lets you share a value with every component beneath a provider without passing props at every level. Create a context with createContext(default), wrap a tree with <MyContext.Provider value={...}>, and read it with useContext(MyContext) anywhere inside. Context is best for low-frequency, app-wide values (theme, current user, locale) — not for state that changes on every keystroke (use Zustand/Redux/Jotai for that, or co-locate state higher up).

tsx
import { createContext, useContext, useState, type ReactNode } from "react";

type Theme = "light" | "dark";
type ThemeContextValue = { theme: Theme; toggle: () => void };

const ThemeContext = createContext<ThemeContextValue | null>(null);

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>("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 used inside ThemeProvider");
  return ctx;
}

// Usage anywhere in the tree
function ThemeToggleButton() {
  const { theme, toggle } = useTheme();
  return <button onClick={toggle}>Theme: {theme}</button>;
}

Output: (after one click)

text
Theme: dark

List rendering and key

When you render an array of items with .map(), React needs a stable, unique key prop on each element so it can match up items across renders and reorder/insert/delete efficiently. Use the item's natural ID — never the array index unless the list is truly static (no inserts, deletes, or reorders), because index keys cause state to leak between items when the list changes.

tsx
type Todo = { id: string; text: string; done: boolean };

export function TodoList({ todos }: { todos: Todo[] }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id} style={{ textDecoration: todo.done ? "line-through" : "none" }}>
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

Output: (rendered)

text
<ul>
  <li style="text-decoration: none;">buy milk</li>
  <li style="text-decoration: line-through;">walk the dog</li>
</ul>

Conditional rendering

JSX is just JavaScript, so any expression works for branching: ternaries, &&, early returns, or helper functions.

tsx
function Status({ user }: { user: { loggedIn: boolean; admin: boolean } | null }) {
  // Early return
  if (!user) return <p>Not signed in.</p>;

  return (
    <>
      {/* Ternary */}
      {user.loggedIn ? <p>Welcome!</p> : <p>Please sign in.</p>}

      {/* && — render-or-nothing */}
      {user.admin && <button>Admin panel</button>}

      {/* Nullish coalescing for empty state */}
      <p>{user.admin ? "admin" : "user"}</p>
    </>
  );
}

Output: (rendered for an admin user)

text
<p>Welcome!</p>
<button>Admin panel</button>
<p>admin</p>

Pitfall: 0 && ...

{count && <X />} renders the number 0 when count is 0 — JSX renders numbers as text. Use count > 0 && <X /> or !!count && <X /> instead.

tsx
{count > 0 && <p>{count} items</p>}

Output: (none — exits 0 on success)

Event handlers and synthetic events

React wraps native DOM events in a cross-browser SyntheticEvent object. Handlers are passed by JSX attribute (onClick, onChange, onSubmit, …). The synthetic event has the same shape as the native one (.target, .preventDefault(), .stopPropagation()) plus React-specific helpers.

tsx
import type { MouseEvent, FormEvent } from "react";

function Button() {
  function handleClick(e: MouseEvent<HTMLButtonElement>) {
    e.preventDefault();
    console.log("clicked", e.currentTarget.dataset.id);
  }

  return <button data-id="42" onClick={handleClick}>Click me</button>;
}

function LoginForm() {
  function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const data = new FormData(e.currentTarget);
    console.log({
      email: data.get("email"),
      password: data.get("password"),
    });
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      <input name="password" type="password" required />
      <button type="submit">Sign in</button>
    </form>
  );
}

Output: (console after submission)

text
{ email: 'alice@example.com', password: 'hunter2' }

Common event types

EventTS type
ClickReact.MouseEvent<HTMLButtonElement>
Input changeReact.ChangeEvent<HTMLInputElement>
Form submitReact.FormEvent<HTMLFormElement>
KeyboardReact.KeyboardEvent<HTMLInputElement>
FocusReact.FocusEvent<HTMLInputElement>

Controlled vs uncontrolled inputs

A controlled input binds its value to state and updates that state in onChange: React is the single source of truth. An uncontrolled input lets the DOM keep the value internally and you read it with a ref or FormData on submit. Prefer controlled inputs when you need live validation, conditional rendering based on the value, or multiple synchronized fields; prefer uncontrolled for plain forms where you only care about the final value at submit time.

tsx
import { useState, useRef } from "react";

// Controlled — value bound to state
export function ControlledEmail() {
  const [email, setEmail] = useState("");
  const valid = email.includes("@");

  return (
    <>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      {!valid && email && <p>Invalid email</p>}
      <button disabled={!valid}>Submit</button>
    </>
  );
}

// Uncontrolled — read via ref
export function UncontrolledEmail() {
  const ref = useRef<HTMLInputElement>(null);

  return (
    <>
      <input ref={ref} type="email" defaultValue="" />
      <button onClick={() => console.log(ref.current?.value)}>
        Log value
      </button>
    </>
  );
}

Output: (console after typing then clicking the uncontrolled button)

text
alice@example.com

A typed form with Zod validation

Combine controlled inputs with Zod for end-to-end type safety: define the schema, derive the type, validate on submit, and render errors.

tsx
import { useState } from "react";
import { z } from "zod";

const SignupSchema = z.object({
  email: z.string().email("must be a valid email"),
  password: z.string().min(8, "at least 8 characters"),
  age: z.coerce.number().int().min(13, "must be 13 or older"),
});

type Signup = z.infer<typeof SignupSchema>;
type Errors = Partial<Record<keyof Signup, string>>;

export function SignupForm() {
  const [form, setForm] = useState({ email: "", password: "", age: "" });
  const [errors, setErrors] = useState<Errors>({});

  function update<K extends keyof typeof form>(key: K, value: string) {
    setForm((prev) => ({ ...prev, [key]: value }));
  }

  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const result = SignupSchema.safeParse(form);
    if (!result.success) {
      const fieldErrors: Errors = {};
      for (const issue of result.error.issues) {
        fieldErrors[issue.path[0] as keyof Signup] = issue.message;
      }
      setErrors(fieldErrors);
      return;
    }
    setErrors({});
    console.log("submit", result.data);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Email
        <input
          value={form.email}
          onChange={(e) => update("email", e.target.value)}
        />
        {errors.email && <small className="err">{errors.email}</small>}
      </label>
      <label>
        Password
        <input
          type="password"
          value={form.password}
          onChange={(e) => update("password", e.target.value)}
        />
        {errors.password && <small className="err">{errors.password}</small>}
      </label>
      <label>
        Age
        <input
          value={form.age}
          onChange={(e) => update("age", e.target.value)}
        />
        {errors.age && <small className="err">{errors.age}</small>}
      </label>
      <button type="submit">Sign up</button>
    </form>
  );
}

Output: (console on a valid submission)

text
submit { email: 'alice@example.com', password: 'hunter2!!', age: 30 }

The React Compiler (overview)

The React Compiler (React 19+) is an opt-in build-time tool that automatically memoizes components and hooks based on dataflow analysis — eliminating most hand-written useMemo / useCallback / memo boilerplate. Add the babel-plugin-react-compiler (or its SWC equivalent in your bundler config); your existing source code is unchanged, and the compiler inserts memoization where it is provably safe. As long as your components follow the Rules of React (no mutation of props/state, no side effects in render), you get free performance with no code changes.

bash
# Babel toolchain
npm install -D babel-plugin-react-compiler

# Or via SWC (Vite)
npm install -D @swc/plugin-react-compiler

Output: (none — exits 0 on success)

typescript
// vite.config.ts — enable in Vite + SWC
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";

export default defineConfig({
  plugins: [
    react({
      plugins: [["@swc/plugin-react-compiler", {}]],
    }),
  ],
});

Common pitfalls

  1. Mutating stateusers.push(x) instead of setUsers([...users, x]). React compares with Object.is; a mutated reference looks unchanged, so no re-render.
  2. Stale closures in effects — referencing state inside a setInterval/setTimeout without putting it in the deps array. Use a functional updater (setX(prev => …)) or add the value to deps.
  3. Missing key or using array index — index keys break when the list reorders. Always use a stable ID.
  4. Component name starts lowercase<myComponent /> is treated as a DOM tag and silently rendered as <mycomponent>. Always capitalize.
  5. useState initial value runs every renderuseState(expensiveCompute()) calls expensiveCompute() on every render (the result is discarded). Use useState(() => expensiveCompute()).
  6. Effect running on every render — forgetting the deps array. useEffect(fn) (no second arg) runs after every render, causing infinite fetches.
  7. Setting state during rendersetX() in the function body without a guard creates an infinite re-render loop. Only call setters from event handlers, effects, or initialization.
  8. {value && <X />} when value is 0 — renders the literal 0. Use value > 0 && <X /> or !!value && <X />.
  9. Forgetting cleanup in effects — subscribers, timers, and AbortControllers must be cleaned up in the returned function or you leak memory / get duplicate handlers in dev (Strict Mode mounts twice).
  10. Conditional hooks — never call a hook inside if/for/early return. React relies on call order; conditional hooks throw "Rendered fewer/more hooks than during the previous render".

Real-world recipes

Debounced search input

A controlled text input that fires an API request 300 ms after the user stops typing — the canonical debounce pattern, expressed as an effect.

tsx
import { useState, useEffect } from "react";

export function SearchBox() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState<string[]>([]);

  useEffect(() => {
    if (!query) {
      setResults([]);
      return;
    }
    const ctrl = new AbortController();
    const id = setTimeout(async () => {
      const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
        signal: ctrl.signal,
      });
      const json: string[] = await res.json();
      setResults(json);
    }, 300);
    return () => {
      clearTimeout(id);
      ctrl.abort();
    };
  }, [query]);

  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="search…" />
      <ul>{results.map((r) => <li key={r}>{r}</li>)}</ul>
    </>
  );
}

Output: (console while typing "ali")

text
GET /api/search?q=ali  -> ["alice", "alicia", "alistair"]

Persistent state with localStorage

A custom hook that mirrors a piece of state to localStorage so it survives reloads — composing useState and useEffect into a reusable building block.

tsx
import { useState, useEffect } from "react";

function usePersistentState<T>(key: string, initial: T): [T, (v: T) => void] {
  const [value, setValue] = useState<T>(() => {
    const cached = localStorage.getItem(key);
    return cached ? (JSON.parse(cached) as T) : initial;
  });

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

  return [value, setValue];
}

// Usage
function App() {
  const [theme, setTheme] = usePersistentState<"light" | "dark">("theme", "light");
  return (
    <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
      Theme: {theme}
    </button>
  );
}

Output: (after toggling and reloading the page)

text
Theme: dark

A reusable modal: rendered into document.body via createPortal, autofocuses its first input on mount, and traps Escape to close. Combines useRef, useEffect, and props.

tsx
import { useEffect, useRef, type ReactNode } from "react";
import { createPortal } from "react-dom";

type ModalProps = { open: boolean; onClose: () => void; children: ReactNode };

export function Modal({ open, onClose, children }: ModalProps) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!open) return;
    ref.current?.querySelector<HTMLElement>("input,button,select,textarea")?.focus();
    const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose();
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [open, onClose]);

  if (!open) return null;
  return createPortal(
    <div className="modal-backdrop" onClick={onClose}>
      <div ref={ref} className="modal" onClick={(e) => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    document.body,
  );
}

Output: (rendered into document.body when open)

text
<div class="modal-backdrop">
  <div class="modal">…</div>
</div>

Pairing with Vite or Bun

The fastest way to start: Vite (most popular) or Bun (single-binary, slightly faster install).

bash
# Vite + React + TypeScript + SWC compiler
npm create vite@latest my-app -- --template react-swc-ts

# Bun + React (uses Bun's native bundler/server)
bun create react my-app

Output:

text
✔ Project name: my-app
✔ Select a framework: › React
✔ Select a variant: › TypeScript + SWC

Scaffolding project in /home/alice/my-app...

Done. Now run:
  cd my-app
  npm install
  npm run dev