React is a JavaScript library for building user interfaces. This guide targets React v18 to v19 (function components and hooks).

Quick start

Installing React

npm install react react-dom

TypeScript projects also need @types/react and @types/react-dom.

Create a root

import { createRoot } from 'react-dom/client'
createRoot(document.getElementById('root')).render(<App />)

The automatic JSX runtime (v17+) means no import React. See: createRoot

StrictMode

<StrictMode><App /></StrictMode>

Double-invokes renders and effects in development to surface impure logic; no effect in production. See: StrictMode

Components

Function components

function Hello({ name }) {
  return (
    <h1 className="greeting">
      Hello {name}
    </h1>
  )
}

Capitalised functions that return JSX. See: Your first component

JSX and conditionals

<label htmlFor="name">Name</label>
<p style={{ color: 'red' }}>{2 + 2}</p>
<>
  {isLoggedIn ? (
    <AdminPanel />
  ) : (
    <LoginButton />
  )}
  {unread > 0 && (
    <Badge count={unread} />
  )}
</>

{} embeds expressions, attributes are camelCase and <>…</> is a fragment; null and booleans render nothing. See: Writing markup with JSX

Props

function Avatar({ src, alt, size = 64 }) {
  return (
    <img
      src={src}
      alt={alt}
      width={size}
      height={size}
    />
  )
}
<Avatar
  src="/me.png"
  alt="Me"
  size={128}
/>

Defaults come from parameters (v19 removed defaultProps); spread extras with {...props}. See: Passing props

Lists and keys

{items.map(item => (
  <li key={item.id}>{item.name}</li>
))}

Keys must be stable and unique among siblings. See: Rendering lists

Events

<button onClick={handleClick}>
  Save
</button>
<button onClick={() => remove(id)}>
  Delete
</button>
<form
  onSubmit={e => {
    e.preventDefault()
    save()
  }}
>
  …
</form>

Pass the handler, not a call, and wrap arguments in an arrow; call e.preventDefault() to stop browser defaults. See: Responding to events

State

useState

const [count, setCount] = useState(0)
setCount(count + 1)
setCount(c => c + 1)  // when the next value depends on the previous
setUser({ ...user, name: 'Ana' })
setItems(items => [...items, item])
setItems(items => items.filter(i => i.id !== id))

State is a per-render snapshot and is immutable — replace it, never mutate in place. See: useState

Controlled inputs

const [name, setName] = useState('')
<input value={name} onChange={e => setName(e.target.value)} />

React owns the value, so the input always reflects state. See: input

Lifting state up

function Parent() {
  const [query, setQuery] = useState('')
  return <Search value={query} onChange={setQuery} />
}

Move shared state to the closest common parent and pass value + setter down. See: Sharing state

useReducer

function reducer(state, action) {
  if (action.type === 'increment') {
    return { count: state.count + 1 }
  }
  return state
}
const [state, dispatch] = useReducer(reducer, { count: 0 })
dispatch({ type: 'increment' })

Prefer a reducer when updates are complex or depend on several values. See: useReducer

Actions (v19)

const [error, submitAction, isPending] =
  useActionState(async (prev, formData) => {
    return (await updateName(formData.get('name'))) ?? null
  }, null)
<form action={submitAction}>
  <input name="name" />
  <button disabled={isPending}>Update</button>
  {error && <p>{error}</p>}
</form>

Actions are async functions in action props; useOptimistic shows an immediate value while the request runs. See: useActionState

Effects

useEffect

useEffect(() => {
  const connection = createConnection(roomId)
  connection.connect()
  return () => connection.disconnect()  // cleanup
}, [roomId])  // re-runs when roomId changes
useEffect(() => {
  const id = setInterval(tick, 1000)
  return () => clearInterval(id)
}, [])  // once on mount; twice in StrictMode dev

Synchronises a component with an external system and cleans up timers, subscriptions and listeners. See: useEffect

Fetching data

useEffect(() => {
  const controller = new AbortController()
  fetch(`/api/users/${id}`, { signal: controller.signal })
    .then(r => {
      if (!r.ok) throw new Error(`HTTP ${r.status}`)
      return r.json()
    })
    .then(setUser)
    .catch(err => { if (err.name !== 'AbortError') setError(err) })
  return () => controller.abort()
}, [id])

Abort in the cleanup to drop stale responses, ignoring the expected AbortError. See: Fetching data

useEffectEvent (v19.2+)

const onConnected = useEffectEvent(() => {
  showNotification('Connected!', theme)
})
useEffect(() => {
  const connection = createConnection(roomId)
  connection.on('connected', onConnected)
  connection.connect()
  return () => connection.disconnect()
}, [roomId])

Reads the latest props and state without re-running the effect, so it stays out of the deps. See: useEffectEvent

You might not need an effect

// prefer: derive during render
const fullName = first + ' ' + last

// avoid: derived state in an effect
useEffect(() => {
  setFullName(first + ' ' + last)
})

Don't use effects to transform data or to react to events. See: You might not need an effect

Refs

useRef

const inputRef = useRef(null)
useEffect(() => inputRef.current.focus(), [])
return <input ref={inputRef} />

A mutable .current box that persists without re-rendering. See: useRef

Ref as a prop (v19)

function Input({ ref, ...props }) {
  return <input ref={ref} {...props} />
}

v19 passes ref as a normal prop; v18 uses forwardRef, no longer necessary in v19 and slated for deprecation. See: forwardRef

useImperativeHandle

useImperativeHandle(ref, () => ({
  play: () => videoRef.current.play()
}), [])

Exposes a custom imperative API instead of the DOM node; rarely needed. See: useImperativeHandle

Portals and inner HTML

import { createPortal } from 'react-dom'
createPortal(<Modal />, document.getElementById('modal-root'))
<div dangerouslySetInnerHTML={{ __html: trustedHtml }} />

A portal renders elsewhere in the DOM while keeping the React tree; dangerouslySetInnerHTML injects raw HTML, so never pass untrusted input. See: createPortal

Context

createContext and useContext

const ThemeContext = createContext(null)
<ThemeContext value="dark"><Toolbar /></ThemeContext>

function Toolbar() {
  const theme = useContext(ThemeContext)
  return <button className={theme}>Hi</button>
}
<ThemeContext value="dark">…</ThemeContext>                     // v19
<ThemeContext.Provider value="dark">…</ThemeContext.Provider>   // v18
function useTheme() {
  const value = useContext(ThemeContext)
  if (value === null) throw new Error('useTheme needs a provider')
  return value
}

Context passes data down without prop drilling and v19 renders <Context> itself as the provider; wrapping useContext names the value and fails loudly outside a provider. See: useContext · createContext

Custom hooks

Call hooks at the top level of a component or hook — never in loops, conditions or nested functions. See: Rules of hooks

useOnlineStatus

function useOnlineStatus() {
  const [isOnline, setOnline] = useState(true)
  useEffect(() => {
    const update = () => setOnline(navigator.onLine)
    update()
    window.addEventListener('online', update)
    window.addEventListener('offline', update)
    return () => {
      window.removeEventListener('online', update)
      window.removeEventListener('offline', update)
    }
  }, [])
  return isOnline
}

A custom hook is just a function that calls other hooks. See: Custom hooks

useId and useDebugValue

const id = useId()
<label htmlFor={id}>Name</label>
<input id={id} />

useId gives SSR-safe ids for accessibility; useDebugValue(label) labels a hook in DevTools. See: useId

Performance

memo, useMemo and useCallback

const Row = memo(function Row({ item }) {
  return <li>{item.name}</li>
})
const visible = useMemo(() => filter(items, query), [items, query])
const onSelect = useCallback(id => setSelected(id), [])

Cache components and values only when measurement shows a need — the React Compiler, once configured in your build, auto-memoises and removes most manual calls. See: memo

useTransition and useDeferredValue

const [isPending, startTransition] = useTransition()
startTransition(() => setQuery(input))
const deferredQuery = useDeferredValue(query)

useTransition marks an update non-blocking so urgent ones (typing) stay responsive; useDeferredValue renders a stale value first, then catches up. See: useTransition · useDeferredValue

useSyncExternalStore

const isOnline = useSyncExternalStore(
  subscribe,
  () => navigator.onLine,
  () => true
)

Subscribes safely to an external store in concurrent rendering; the third argument is the server snapshot. See: useSyncExternalStore

Suspense and lazy

lazy and Suspense

const Chart = lazy(() => import('./Chart'))
<Suspense fallback={<Spinner />}><Chart /></Suspense>

Code-splits a component; the fallback shows while it loads. See: Suspense

use(promise) (v19)

function Comments({ commentsPromise }) {
  const comments = use(commentsPromise)
  return comments.map(c => <p key={c.id}>{c.text}</p>)
}

Reads a promise or context during render and suspends; unlike hooks it may be conditional. See: use

Rendering and SSR

Client rendering

// fresh DOM
createRoot(document.getElementById('root'))
  .render(<App />)

// server HTML
hydrateRoot(
  document.getElementById('root'),
  <App />
)

Hydration markup must match the client render. See: Client APIs

Server rendering

import { renderToString } from 'react-dom/server'
const html = renderToString(<App />)  // synchronous, blocks

renderToPipeableStream (Node) and renderToReadableStream (web) stream with Suspense; v19 adds prerender. See: Server APIs

Error handling

Error boundaries

class ErrorBoundary extends Component {
  state = { error: null }

  static getDerivedStateFromError(error) {
    return { error }
  }

  componentDidCatch(error, info) {
    logError(error, info)
  }

  render() {
    if (this.state.error) {
      return <h1>Something went wrong.</h1>
    }
    return this.props.children
  }
}

Still the only built-in way to catch render errors, and still a class; react-error-boundary wraps it with a hooks-friendly API. See: Error boundaries

Legacy APIs

Rendering and lifecycle

LegacyModern
ReactDOM.render(el, node)createRoot(node).render(el)
ReactDOM.hydrate(el, node)hydrateRoot(node, el)
componentDidMount / componentDidUpdateuseEffect
componentWillUnmounteffect cleanup
Class componentsfunction components

Components and props

LegacyModern
defaultProps (function components)default parameter values
propTypesTypeScript
React.createElementJSX

Refs and context

LegacyModern
String refs ref="input"ref callback or useRef
forwardRef (v18)ref as a prop (v19)
Legacy context (getChildContext)createContext
<Context.Provider value> (v18)<Context value> (v19)

Further reading

See: Legacy APIs. Classes still work and are required only for error boundaries; see the React v16 cheatsheet for the class-era API.

Also see

0 Comments for this cheatsheet. Write yours!