React Quick Reference

Components, JSX, hooks, state management, event handling, forms, context API and common patterns.

Component Basics

// Function component (recommended)
function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

// Arrow function component
const Greeting = ({ name }) => <h1>Hello, {name}!</h1>;

// With TypeScript
interface GreetingProps {
  name: string;
  age?: number;
}

const Greeting: React.FC<GreetingProps> = ({ name, age }) => (
  <div>
    <h1>Hello, {name}!</h1>
    {age && <p>Age: {age}</p>}
  </div>
);

// Component with children
function Card({ title, children }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
    </div>
  );
}

// Usage
<Card title="Welcome">
  <p>This is the card content.</p>
</Card>

// Fragment — return multiple elements without a wrapper
function List() {
  return (
    <>
      <li>Item 1</li>
      <li>Item 2</li>
    </>
  );
}

JSX Syntax

// Expressions in JSX — wrap in curly braces
<p>{user.name}</p>
<p>{2 + 2}</p>
<p>{isLoggedIn ? 'Welcome back' : 'Please log in'}</p>

// className instead of class
<div className="container"></div>

// htmlFor instead of for
<label htmlFor="email">Email</label>

// Style — object with camelCase properties
<div style={{ backgroundColor: '#0d1117', fontSize: '1rem' }}></div>

// Boolean attributes
<input disabled />
<button disabled={isLoading}>Submit</button>

// Spread attributes
const props = { id: 'main', className: 'wrapper', role: 'main' };
<div {...props}></div>

// Dangerously set HTML (use with caution)
<div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />

// Self-closing tags required for void elements
<img src="photo.jpg" alt="Photo" />
<br />
<input type="text" />

Props & State

// Props — read-only data passed from parent to child
function UserCard({ name, email, avatar = '/default.png' }) {
  return (
    <div className="user-card">
      <img src={avatar} alt={name} />
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  );
}

// Destructuring with rest
function Button({ children, variant = 'primary', ...rest }) {
  return (
    <button className={`btn btn-${variant}`} {...rest}>
      {children}
    </button>
  );
}

// State — mutable data managed within a component
import { useState } from 'react';

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

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(prev => prev - 1)}>−</button>
    </div>
  );
}

// State with objects — always create new references
const [user, setUser] = useState({ name: '', email: '' });
setUser(prev => ({ ...prev, name: 'Alex' }));

// State with arrays
const [items, setItems] = useState([]);
setItems(prev => [...prev, newItem]);                    // add
setItems(prev => prev.filter(item => item.id !== id));  // remove
setItems(prev => prev.map(item =>
  item.id === id ? { ...item, done: true } : item        // update
));

// Lifting state up — share state via common ancestor
function Parent() {
  const [value, setValue] = useState('');
  return (
    <>
      <InputField value={value} onChange={setValue} />
      <Display value={value} />
    </>
  );
}

Hooks

useState

const [state, setState] = useState(initialValue);

// Lazy initialization — runs only on first render
const [data, setData] = useState(() => expensiveComputation());

useEffect

import { useEffect } from 'react';

// Runs after every render
useEffect(() => {
  document.title = `Count: ${count}`;
});

// Runs only on mount (empty dependency array)
useEffect(() => {
  const controller = new AbortController();
  fetch('/api/data', { signal: controller.signal })
    .then(res => res.json())
    .then(setData);

  return () => controller.abort();  // cleanup on unmount
}, []);

// Runs when dependencies change
useEffect(() => {
  fetchUser(userId).then(setUser);
}, [userId]);

// Cleanup — return a function to run on unmount or before re-run
useEffect(() => {
  const id = setInterval(() => tick(), 1000);
  return () => clearInterval(id);
}, []);

useContext

import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button style={{ background: theme.primary }}>Click</button>;
}

useRef

import { useRef } from 'react';

// DOM reference
function TextInput() {
  const inputRef = useRef(null);

  const focusInput = () => inputRef.current?.focus();

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus</button>
    </>
  );
}

// Mutable value that persists across renders (no re-render on change)
const renderCount = useRef(0);
useEffect(() => { renderCount.current += 1; });

// Previous value pattern
function usePrevious(value) {
  const ref = useRef();
  useEffect(() => { ref.current = value; });
  return ref.current;
}

useMemo & useCallback

import { useMemo, useCallback } from 'react';

// useMemo — memoize expensive computations
const sortedItems = useMemo(() => {
  return items.slice().sort((a, b) => a.name.localeCompare(b.name));
}, [items]);

// useCallback — memoize function references (prevents child re-renders)
const handleDelete = useCallback((id) => {
  setItems(prev => prev.filter(item => item.id !== id));
}, []);

// useCallback is equivalent to useMemo returning a function
const fn = useMemo(() => () => doSomething(a, b), [a, b]);
const fn = useCallback(() => doSomething(a, b), [a, b]);

useReducer

import { useReducer } from 'react';

const initialState = { count: 0, step: 1 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + state.step };
    case 'decrement':
      return { ...state, count: state.count - state.step };
    case 'setStep':
      return { ...state, step: action.payload };
    case 'reset':
      return initialState;
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count} (step: {state.step})</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>−</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  );
}

Event Handling

// onClick
<button onClick={() => console.log('clicked')}>Click</button>

// With event object
function handleClick(e) {
  e.preventDefault();
  console.log('Button clicked');
}
<button onClick={handleClick}>Click</button>

// Passing arguments
<button onClick={() => deleteItem(item.id)}>Delete</button>

// onChange for inputs
<input
  type="text"
  value={name}
  onChange={(e) => setName(e.target.value)}
/>

// onSubmit for forms
<form onSubmit={(e) => {
  e.preventDefault();
  handleSubmit(formData);
}}>

// Keyboard events
<input onKeyDown={(e) => {
  if (e.key === 'Enter') handleSubmit();
}} />

// TypeScript event types
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  setValue(e.target.value);
};

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
};

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
  if (e.key === 'Enter') submit();
};

Conditional Rendering

// Ternary operator
{isLoggedIn ? <Dashboard /> : <Login />}

// Logical AND — renders when condition is truthy
{isAdmin && <AdminPanel />}
{items.length > 0 && <ItemList items={items} />}

// Logical AND pitfall — 0 renders as "0", use boolean conversion
{!!count && <Badge count={count} />}
{count > 0 && <Badge count={count} />}

// Nullish coalescing for fallbacks
{user?.name ?? 'Anonymous'}

// Early return pattern
function UserProfile({ user }) {
  if (!user) return <p>Loading...</p>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

// Variable assignment
function StatusIcon({ status }) {
  let icon;
  if (status === 'success') icon = <CheckIcon />;
  else if (status === 'error') icon = <ErrorIcon />;
  else icon = <SpinnerIcon />;

  return <span className="status">{icon}</span>;
}

// Render nothing
function MaybeVisible({ show, children }) {
  if (!show) return null;
  return children;
}

Lists & Keys

// Basic list rendering
function UserList({ users }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// With index as key (only when items have no stable ID and list is static)
{items.map((item, index) => (
  <li key={index}>{item}</li>
))}

// Extracting list item components
function TodoItem({ todo, onToggle }) {
  return (
    <li onClick={() => onToggle(todo.id)}>
      <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
        {todo.text}
      </span>
    </li>
  );
}

function TodoList({ todos, onToggle }) {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem key={todo.id} todo={todo} onToggle={onToggle} />
      ))}
    </ul>
  );
}

// Filtering and mapping
{users
  .filter(u => u.active)
  .map(u => <UserCard key={u.id} user={u} />)
}

// Empty state
{items.length === 0
  ? <p className="empty">No items yet.</p>
  : items.map(item => <Item key={item.id} {...item} />)
}
Keys must be stable, unique among siblings, and should not change between renders. Use database IDs or unique identifiers — avoid array indices for dynamic lists where items can be reordered, added or removed.

Forms

// Controlled component
function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    login({ email, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="email">Email</label>
      <input
        id="email"
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        required
      />

      <label htmlFor="password">Password</label>
      <input
        id="password"
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        required
      />

      <button type="submit">Log In</button>
    </form>
  );
}

// Multiple fields with single state object
function ProfileForm() {
  const [form, setForm] = useState({ name: '', bio: '', website: '' });

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

  return (
    <form>
      <input name="name" value={form.name} onChange={handleChange} />
      <textarea name="bio" value={form.bio} onChange={handleChange} />
      <input name="website" value={form.website} onChange={handleChange} />
    </form>
  );
}

// Uncontrolled component with useRef
function FileUpload() {
  const fileRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    const file = fileRef.current.files[0];
    uploadFile(file);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="file" ref={fileRef} />
      <button type="submit">Upload</button>
    </form>
  );
}

// Select, checkbox, radio
<select value={role} onChange={(e) => setRole(e.target.value)}>
  <option value="admin">Admin</option>
  <option value="editor">Editor</option>
  <option value="viewer">Viewer</option>
</select>

<input type="checkbox" checked={agreed} onChange={(e) => setAgreed(e.target.checked)} />

<input type="radio" name="plan" value="free" checked={plan === 'free'} onChange={(e) => setPlan(e.target.value)} />

Context API

import { createContext, useContext, useState } from 'react';

// 1. Create context with default value
const ThemeContext = createContext({
  theme: 'dark',
  toggleTheme: () => {}
});

// 2. Provider component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('dark');

  const toggleTheme = () => {
    setTheme(prev => (prev === 'dark' ? 'light' : 'dark'));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. Custom hook for consuming context
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

// 4. Use in components
function Header() {
  const { theme, toggleTheme } = useTheme();

  return (
    <header className={`header-${theme}`}>
      <button onClick={toggleTheme}>
        Switch to {theme === 'dark' ? 'light' : 'dark'}
      </button>
    </header>
  );
}

// 5. Wrap your app
function App() {
  return (
    <ThemeProvider>
      <Header />
      <Main />
    </ThemeProvider>
  );
}
Context is ideal for global data like themes, auth state and locale. For high-frequency updates, consider using state management libraries (Zustand, Jotai) or splitting context into separate providers to minimize re-renders.

Common Patterns

Custom Hooks

// useFetch — reusable data fetching
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    setLoading(true);

    fetch(url, { signal: controller.signal })
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(setData)
      .catch(err => {
        if (err.name !== 'AbortError') setError(err);
      })
      .finally(() => setLoading(false));

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

  return { data, loading, error };
}

// useLocalStorage — persist state to localStorage
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

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

  return [value, setValue];
}

// useDebounce
function useDebounce(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debounced;
}

Error Boundaries

import { Component } from 'react';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

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

  componentDidCatch(error, errorInfo) {
    console.error('Error boundary caught:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || <h2>Something went wrong.</h2>;
    }
    return this.props.children;
  }
}

// Usage
<ErrorBoundary fallback={<p>Failed to load.</p>}>
  <UserProfile />
</ErrorBoundary>

Code Splitting

import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}