Skip to main content

🧱 Part 4 — State Management 101 (useState, useReducer & Custom Hooks) ⚡

 State is the memory of your components. In this part, we’ll:

  • Learn when to use useState vs useReducer.
  • Build typed reducers.
  • Create custom hooks for reusable state logic.

🎯 1. Recap: useState

You already know useState holds a single value:

import { useState } from 'react';

export function Counter() {
  // count: number, setCount: function to update it
  const [count, setCount] = useState(0); // inferred as number

  return (
    <div>
      <p>Count: {count}</p>
      {/* onClick updates state */}
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

👉 Perfect for simple local state.


🎯 2. When to use useReducer

If state has multiple fields or complex updates, useReducer is better.

Example: a Task object.

// Define a Task type
type Task = { id: string; title: string; done: boolean };

// State is an array of tasks
type State = Task[];

// Define all possible actions
type Action =
  | { type: 'add'; title: string }
  | { type: 'toggle'; id: string }
  | { type: 'remove'; id: string };

// Reducer function: receives current state + action → returns new state
function tasksReducer(state: State, action: Action): State {
  switch (action.type) {
    case 'add':
      // create new task with random id
      return [...state, { id: crypto.randomUUID(), title: action.title, done: false }];
    case 'toggle':
      // flip the "done" flag
      return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t);
    case 'remove':
      // remove task by id
      return state.filter(t => t.id !== action.id);
    default: {
      // exhaustive check: ensures all Action types are handled
      const _exhaustive: never = action;
      return state;
    }
  }
}

Usage:

import { useReducer, useState } from 'react';

export function TaskList() {
  // tasks: State, dispatch: function to send actions
  const [tasks, dispatch] = useReducer(tasksReducer, []);
  const [title, setTitle] = useState('');

  const addTask = () => {
    if (title.trim()) {
      dispatch({ type: 'add', title }); // dispatch an "add" action
      setTitle('');
    }
  };

  return (
    <div>
      {/* controlled input */}
      <input value={title} onChange={e => setTitle(e.target.value)} />
      <button onClick={addTask}>Add</button>
      <ul>
        {tasks.map(t => (
          <li key={t.id}>
            <label>
              {/* toggle checkbox dispatches a "toggle" action */}
              <input
                type="checkbox"
                checked={t.done}
                onChange={() => dispatch({ type: 'toggle', id: t.id })}
              />
              {t.title}
            </label>
            {/* delete button dispatches a "remove" action */}
            <button onClick={() => dispatch({ type: 'remove', id: t.id })}>❌</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

👉 With useReducer, your state updates are predictable, and TypeScript ensures actions are always valid.


🎯 3. Extracting Logic with Custom Hooks

If multiple components need the same logic, extract it.

// src/hooks/useTasks.ts
import { useReducer } from 'react';

// Define Task type
type Task = { id: string; title: string; done: boolean };

// State is array of tasks
type State = Task[];

// All actions our reducer can handle
type Action =
  | { type: 'add'; title: string }
  | { type: 'toggle'; id: string }
  | { type: 'remove'; id: string };

// Reducer function
function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'add':
      return [...state, { id: crypto.randomUUID(), title: action.title, done: false }];
    case 'toggle':
      return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t);
    case 'remove':
      return state.filter(t => t.id !== action.id);
    default:
      return state; // fallback (shouldn’t happen with exhaustive typing)
  }
}

// Custom hook that encapsulates tasks logic
export function useTasks(initial: Task[] = []) {
  const [tasks, dispatch] = useReducer(reducer, initial);
  // return both tasks state and dispatcher to the caller
  return { tasks, dispatch };
}

Usage:

import { useTasks } from '@/hooks/useTasks';

export function TaskApp() {
  const { tasks, dispatch } = useTasks();

  return (
    <ul>
      {tasks.map(t => (
        <li key={t.id}>{t.title}</li>
      ))}
    </ul>
  );
}

👉 Now any component can use useTasks and get a fully typed task manager.


🎯 4. Visual Example for Blog Readers

Show before/after:

  • Before → scattered state logic in each component.
  • After → clean reducer + hook that centralizes state logic.

✅ Wrap-Up

In Part 4, you learned:

  • When to use useState vs useReducer.
  • How to create a typed reducer with exhaustive checks.
  • How to move logic into a custom hook for reusability.

👉 Next: We’ll explore Routing with React Router — setting up pages, navigation, and type-safe route parameters.

Comments

Popular posts from this blog

🚪 Part 9: API Gateway for .NET 8 Microservices (Ocelot & YARP)

Once you have multiple microservices (Products, Orders, Payments…), exposing each one directly to clients gets messy: Different base URLs Duplicated auth logic No unified rate limiting / caching Hard to evolve routes or aggregate data 👉 Enter the API Gateway — your single front door for all microservices. An API Gateway handles: ✅ Routing & path rewriting ✅ Load balancing, retries, circuit breakers ✅ Authentication & Authorization (JWT, OAuth2) ✅ Rate limiting & caching ✅ Aggregation (compose results from multiple services) In this post we’ll implement two strong options: Ocelot → config-driven, mature, DevOps-friendly YARP (Yet Another Reverse Proxy) → Microsoft’s code-first, extensible gateway ⚖️ Ocelot vs YARP — When to Choose Ocelot → JSON config, minimal C#, built-in QoS (rate limit, circuit breaker). Perfect for teams that like DevOps config-as-code. YARP → full C# control, middleware-friendly, can embed into broader apps (e.g. add dashb...

🌟 Dot net Microservices interview questions

Here is a comprehensive list of 200 .NET microservices coding questions covering all core microservices concepts and cross-cutting concerns relevant for designing, building, deploying, and maintaining .NET-based distributed systems. 🧩 A. Microservices Fundamentals (20) Build a microservice in .NET 8 that exposes a simple CRUD API. Implement communication between two microservices using REST. How would you design microservices for an e-commerce application? Create a microservice that handles user registration and login. How do you isolate domain logic in a microservice? How to apply the "Single Responsibility Principle" in microservices? Design a service registry/discovery mechanism using custom middleware. Implement a service that handles file uploads and metadata separately. Build a stateless microservice and explain its benefits. Implement health check endpoints in .NET 8. Demonstrate versioning in a microservice API. Add Swagger/OpenAPI support to your m...

🎯 C# and Dot net important concepts for interview

🧭 Comprehensive Guide to C# and .NET Concepts This post is your structured map of all major C# language features and the .NET ecosystem. Whether you're preparing for interviews, certifications, or aiming to become a senior .NET developer, this list provides a clear roadmap from fundamentals to advanced topics. 1. C# Language Fundamentals Syntax basics → identifiers, keywords, statements, operators, precedence Types → value types (structs), reference types (classes), nullable value types, records Variables & scope → local, fields, constants, readonly, ref/out/in Control flow → if, switch (pattern matching), loops (for/foreach/while/do), goto Methods → parameters (default/named), overloading, optional params, discards Strings & text → string, StringBuilder, interpolation, raw string literals, ReadOnlySpan<char> Arrays & collections → arrays, tuples, ranges/indices, List, Dictionary, HashSet, LinkedList Exceptions → try/catch/finally, throw, custom exceptions, hiera...