0Pricing
TypeScript Academy · Lesson

State & reducers; Context typing

Manage component state with useState and useReducer, then lift state into a typed Context with a safe custom hook.

State & reducers; Context typing is a free TypeScript Academy lesson on CoddyKit — lesson 2 of 3. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the TypeScript Academy learning path, one of 3 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Intro

Goal: Start with local state (useState), move to a typed reducer (useReducer), then lift state to a typed Context with a safe custom hook.

  • Model state and actions explicitly
  • Prefer discriminated unions
  • Guard Context access

useState typed

Annotate state with a concrete type (e.g., number). Use the updater form to avoid stale closures in rapid updates.

import React, { useState } from "react"

export default function Counter() {
  const [count, setCount] = useState<number>(0)

  const inc = () => setCount(c => c + 1)
  const dec = () => setCount(c => c - 1)

  return (
    <div style={{ display: "grid", gap: 8 }}>
      <div>Count: {count}</div>
      <div style={{ display: "flex", gap: 8 }}>
        <button onClick={inc}>+1</button>
        <button onClick={dec}>-1</button>
      </div>
    </div>
  )
}

useReducer typed

Model Action as a discriminated union. The switch stays exhaustive; payloads are typed per branch.

import React, { useReducer } from "react"

type State = { count: number }

type Action =
  | { type: "inc" }
  | { type: "add"; by: number }
  | { type: "reset" }

function reducer(s: State, a: Action): State {
  switch (a.type) {
    case "inc": return { count: s.count + 1 }
    case "add": return { count: s.count + a.by }
    case "reset": return { count: 0 }
  }
}

export default function App() {
  const [state, dispatch] = useReducer(reducer, { count: 0 })
  return (
    <div style={{ display: "grid", gap: 8 }}>
      <div>Count: {state.count}</div>
      <button onClick={() => dispatch({ type: "inc" })}>+1</button>
      <button onClick={() => dispatch({ type: "add", by: 5 })}>+5</button>
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
    </div>
  )
}

Context + hook

Create Context<T | null>, then expose useX that throws if null. Consumers stay simple and well-typed.

import React, { createContext, useContext, useReducer } from "react"

type State = { count: number }

type Action = { type: "inc" } | { type: "add"; by: number } | { type: "reset" }

function reducer(s: State, a: Action): State {
  switch (a.type) {
    case "inc": return { count: s.count + 1 }
    case "add": return { count: s.count + a.by }
    case "reset": return { count: 0 }
  }
}

type Ctx = { state: State; dispatch: React.Dispatch<Action> }

const CounterCtx = createContext<Ctx | null>(null)

export function useCounter() {
  const ctx = useContext(CounterCtx)
  if (!ctx) throw new Error("useCounter must be used within CounterProvider")
  return ctx
}

export function CounterProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(reducer, { count: 0 })
  return <CounterCtx.Provider value={{ state, dispatch }}>{children}</CounterCtx.Provider>
}

export default function App() {
  return (
    <CounterProvider>
      <Inner />
    </CounterProvider>
  )
}

function Inner() {
  const { state, dispatch } = useCounter()
  return (
    <div style={{ display: "grid", gap: 8 }}>
      <div>Count: {state.count}</div>
      <button onClick={() => dispatch({ type: "inc" })}>+1</button>
    </div>
  )
}

Context + reducer pattern

Split reading and dispatching across components. Provider wraps children and supplies typed state + dispatch.

import React from "react"
import { CounterProvider, useCounter } from "./ctx"

function Controls() {
  const { dispatch } = useCounter()
  return (
    <div style={{ display: "flex", gap: 8 }}>
      <button onClick={() => dispatch({ type: "inc" })}>+1</button>
      <button onClick={() => dispatch({ type: "add", by: 10 })}>+10</button>
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
    </div>
  )
}

function View() {
  const { state } = useCounter()
  return <div>Count: {state.count}</div>
}

export default function App() {
  return (
    <CounterProvider>
      <div style={{ display: "grid", gap: 8 }}>
        <View />
        <Controls />
      </div>
    </CounterProvider>
  )
}

Best practices

Best practices:

  • Keep state minimal and derive the rest.
  • Prefer discriminated unions for actions.
  • Protect Context access with a custom hook.

Context typing check

Quick check: What is a safe way to type a React Context?

Recap

Recap: Start with useState, model transitions with useReducer, and lift shared state into a typed Context guarded by a custom hook.

Frequently asked questions

Is the “State & reducers; Context typing” lesson free?

Yes — the full text of “State & reducers; Context typing” is free to read here on the web, and the TypeScript Academy course includes 3 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the TypeScript Academy course, upgrade to CoddyKit PRO.

What will I learn in “State & reducers; Context typing”?

Manage component state with useState and useReducer, then lift state into a typed Context with a safe custom hook. You practise TypeScript Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start TypeScript Academy?

No prior experience is required. TypeScript Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 3, so you can start here or from the beginning and move at your own pace.

How long does the “State & reducers; Context typing” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this TypeScript Academy lesson?

Yes. Every TypeScript Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Props & children; event handlers; refs
  2. State & reducers; Context typing
  3. Component generics — intro
← Back to TypeScript Academy