javascript 41 lines · 9 steps

Building a data-fetching hook in React

A custom React hook that models async fetch state with a reducer and cancels stale requests.

Explained by highlit
1import { useReducer, useEffect } from "react";
2 
3const initialState = { status: "idle", data: null, error: null };
4 
5function reducer(state, action) {
6 switch (action.type) {
7 case "FETCH_START":
8 return { ...state, status: "loading", error: null };
9 case "FETCH_SUCCESS":
10 return { status: "success", data: action.payload, error: null };
11 case "FETCH_ERROR":
12 return { ...state, status: "error", error: action.error };
13 default:
14 return state;
15 }
16}
17 
18export function useUserProfile(userId) {
19 const [state, dispatch] = useReducer(reducer, initialState);
20 
21 useEffect(() => {
22 const controller = new AbortController();
23 
24 dispatch({ type: "FETCH_START" });
25 
26 fetch(`/api/users/${userId}`, { signal: controller.signal })
27 .then((res) => {
28 if (!res.ok) throw new Error(`Request failed with ${res.status}`);
29 return res.json();
30 })
31 .then((data) => dispatch({ type: "FETCH_SUCCESS", payload: data }))
32 .catch((err) => {
33 if (err.name === "AbortError") return;
34 dispatch({ type: "FETCH_ERROR", error: err.message });
35 });
36 
37 return () => controller.abort();
38 }, [userId]);
39 
40 return state;
41}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Modeling async status as an explicit state machine beats juggling separate loading and error booleans.
  2. 2An AbortController cleanup keeps a stale request from writing state after the inputs change.
  3. 3A reducer plus useEffect packages fetch lifecycle logic into one reusable hook.

Related explainers

javascript
const ROLE_PERMISSIONS = {
  admin: ['users:read', 'users:write', 'billing:read', 'billing:write'],
  manager: ['users:read', 'billing:read'],
  member: ['users:read'],

Role-based permissions middleware in Express

authorization middleware rbac
Intermediate 9 steps
javascript
function attachThousandSeparators(input, { locale = 'en-US' } = {}) {
  const formatter = new Intl.NumberFormat(locale);
  const groupSep = formatter.format(11111).replace(/\d/g, '')[0] || ',';
  const decimalSep = formatter.format(1.1).replace(/\d/g, '')[0] || '.';

Live thousand separators without losing the caret

dom intl caret-preservation
Advanced 8 steps
javascript
const express = require('express');
const app = express();
 
app.get('/health', (req, res) => res.json({ status: 'ok' }));

Graceful shutdown in an Express server

graceful-shutdown signal-handling connection-tracking
Advanced 9 steps
javascript
import { useState, useEffect, useCallback } from 'react';
 
function getColumnCount(width) {
  if (width < 640) return 1;

A responsive column hook in React

custom-hooks debouncing responsive-design
Intermediate 7 steps
javascript
function initCharacterCounter(textarea, options = {}) {
  const maxLength = options.maxLength ?? 280;
  const warnThreshold = options.warnThreshold ?? 0.9;
 

A live character counter for textareas

dom closures accessibility
Intermediate 7 steps
javascript
function autoResizeTextarea(textarea, { maxHeight = Infinity } = {}) {
  const resize = () => {
    textarea.style.height = 'auto';
    const contentHeight = textarea.scrollHeight;

Auto-resizing a textarea to fit its content

dom event-listener cleanup
Intermediate 7 steps

Share this explainer

Here's the card — post it anywhere.

Building a data-fetching hook in React — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code