Typorithm · hard

7. Typed state, reducers, refs and effects

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

7. Typed state, reducers, refs and effects

0%

Concept and contract

Use hooks with honest state models and testable transitions.

useState infers from its initial value, but an empty array or null often lacks the complete future state vocabulary. Supply an explicit type when the state must later hold roles or a nullable element. Do not add redundant annotations when inference already captures the intended contract.

A reducer is useful when multiple events must preserve a state invariant. Model events as a discriminated union and test the reducer as a pure function. A ref holds mutable information across renders without causing a render when its current value changes.

Mechanism and worked example

The example’s reducer handles add and reset events with required payloads.

The example’s reducer handles add and reset events with required payloads. useReducer connects the state and event contracts to dispatch. The implementation constructs new arrays instead of mutating the existing state, so React can observe the state change.

Effects synchronize with external systems; they are not required to derive a filtered array from current props and state. React development Strict Mode can run an extra setup-and-cleanup cycle. Avoid teaching an empty dependency list as an unconditional guarantee of exactly one lifetime execution.

Complete worked example

typescript

import { useReducer } from "react";
type Action = {type:"add";title:string}|{type:"reset"};
function reducer(state:string[],action:Action):string[]{return action.type === "reset" ? [] : [...state,action.title];}
export function Titles(){const [titles,dispatch]=useReducer(reducer,[]);return <button onClick={()=>dispatch({type:"add",title:"QA"})}>{titles.length}</button>;}
Reasoning
The action union and immutable reducer connect dispatched events to valid next states.

Counterexamples and common mistakes

The rejected add event omits its required title.

The rejected add event omits its required title. An optional title would weaken the invariant and force every reducer branch to defend against an event that should never be constructed. Keep action payloads required where their event needs them.

Do not use a ref to hide state that must update the rendered screen. Conversely, avoid state for an imperative timer id that does not affect rendering. TypeScript cannot verify effect dependency completeness or prevent stale closures; lint rules and behavioral tests remain important.

Rejected counterexample: expected compiler diagnostic

typescript

type Action = {type:"add";title:string}|{type:"reset"};
// @ts-expect-error An add action requires its payload.
const invalid:Action={type:"add"};
Reasoning
A required payload prevents incomplete add actions from reaching the reducer.

Independent practice

Implement a reducer with saved role ids and toggle events.

Implement a reducer with saved role ids and toggle events. Toggling an absent id adds it; toggling a present id removes it. Keep state immutable and reject malformed event construction. Begin with a pure reducer before wiring useReducer.

Test add, remove and an unrelated existing id. Optional extension: store a nullable input ref and focus it through an effect or explicit user action. Keep that imperative extension separate from the core state-transition exercise.

Step 1: Add an absent id immutably. Step 2: Remove a present id without changing source state.

Practice starter: solve before reading the solution

typescript

type Toggle = {type:"toggle";id:string};
function savedReducer(state:readonly string[],action:Toggle):string[]{throw new Error("TODO");}
Reasoning
Solve and test the pure state transition before adding the hook wrapper.
  • Hint 1: Test the pure reducer before the hook wrapper.
  • Hint 2: Refs do not schedule renders when written.

Solution, tests and trade-offs

The solution uses includes and filter or spread to create a new array.

The solution uses includes and filter or spread to create a new array. The event’s required id supplies the operation’s input. React can consume this reducer directly, while its correctness can be tested without mounting a component.

Assert both returned ids and the unchanged source array. Then mount a tiny harness and toggle twice to verify rendered behavior. Compiler acceptance alone does not prove rerendering, effect cleanup or progress persistence in an application.

Practice solution and acceptance checks

typescript

type Toggle = {type:"toggle";id:string};
function savedReducer(state:readonly string[],action:Toggle):string[]{
  return state.includes(action.id) ? state.filter(id=>id!==action.id) : [...state,action.id];
}

// Acceptance checks
const input=["a"];if(savedReducer(input,{type:"toggle",id:"b"}).join(",")!=="a,b"||savedReducer(input,{type:"toggle",id:"a"}).length!==0||input.join(",")!=="a")throw new Error("toggle");
Reasoning
A new array records the toggle while retaining caller-owned state unchanged.

Interview reasoning and next step

In interviews, explain when state, reducer and ref are appropriate, and give one example of derived data that needs no effect.

In interviews, explain when state, reducer and ref are appropriate, and give one example of derived data that needs no effect. Discuss development Strict Mode honestly instead of using a once-only lifecycle slogan.

Next, apply these contracts to forms, where text parsing and submission state require runtime checks even when every prop and hook call is correctly typed.

Optional stretch — after the core task works: Connect an input ref to an explicit focus action.

Use hooks with honest state models and testable transitions.