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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A lazy state initializer lets you compute the first value once while guarding against undefined APIs during server rendering.
- 2Debouncing plus requestAnimationFrame keeps resize handlers cheap by collapsing bursts into a single paint-aligned update.
- 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.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/a-responsive-column-hook-in-react-explained-javascript-57b5/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.