javascript 42 lines · 7 steps

A responsive column hook in React

A custom hook that tracks viewport width and returns how many columns to render, with debounced, SSR-safe updates.

Explained by highlit
1import { useState, useEffect, useCallback } from 'react';
2 
3function getColumnCount(width) {
4 if (width < 640) return 1;
5 if (width < 1024) return 2;
6 if (width < 1440) return 3;
7 return 4;
8}
9 
10export function useResponsiveColumns() {
11 const [columns, setColumns] = useState(() =>
12 getColumnCount(typeof window !== 'undefined' ? window.innerWidth : 1440)
13 );
14 
15 const recompute = useCallback(() => {
16 setColumns((prev) => {
17 const next = getColumnCount(window.innerWidth);
18 return next === prev ? prev : next;
19 });
20 }, []);
21 
22 useEffect(() => {
23 let frame;
24 let timeout;
25 
26 const handleResize = () => {
27 clearTimeout(timeout);
28 timeout = setTimeout(() => {
29 frame = requestAnimationFrame(recompute);
30 }, 150);
31 };
32 
33 window.addEventListener('resize', handleResize);
34 return () => {
35 window.removeEventListener('resize', handleResize);
36 clearTimeout(timeout);
37 cancelAnimationFrame(frame);
38 };
39 }, [recompute]);
40 
41 return columns;
42}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1A lazy state initializer lets you compute the first value once while guarding against undefined APIs during server rendering.
  2. 2Debouncing plus requestAnimationFrame keeps resize handlers cheap by collapsing bursts into a single paint-aligned update.
  3. 3Every listener and timer created in an effect should be torn down in its cleanup to avoid leaks and stale callbacks.

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
import { useReducer, useEffect } from "react";
 
const initialState = { status: "idle", data: null, error: null };
 

Building a data-fetching hook in React

custom-hooks usereducer data-fetching
Intermediate 9 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
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.

A responsive column hook in React — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code