javascript 29 lines · 6 steps

A React hook that refetches on tab focus

A custom hook re-runs a fetch when the user returns to the tab, but only if the data is stale.

Explained by highlit
1import { useEffect, useRef } from 'react';
2 
3export function useRefetchOnFocus(refetch, { staleTime = 30_000 } = {}) {
4 const lastFetchedAt = useRef(Date.now());
5 const refetchRef = useRef(refetch);
6 refetchRef.current = refetch;
7 
8 useEffect(() => {
9 const maybeRefetch = () => {
10 if (document.visibilityState !== 'visible') return;
11 if (Date.now() - lastFetchedAt.current < staleTime) return;
12 
13 lastFetchedAt.current = Date.now();
14 refetchRef.current();
15 };
16 
17 document.addEventListener('visibilitychange', maybeRefetch);
18 window.addEventListener('focus', maybeRefetch);
19 
20 return () => {
21 document.removeEventListener('visibilitychange', maybeRefetch);
22 window.removeEventListener('focus', maybeRefetch);
23 };
24 }, [staleTime]);
25 
26 return () => {
27 lastFetchedAt.current = Date.now();
28 };
29}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1A ref holding the latest callback lets an effect stay subscribed without re-binding listeners on every render.
  2. 2Tracking a last-fetched timestamp turns focus events into throttled, staleness-aware refetches.
  3. 3Effects that add listeners must return a cleanup that removes them to avoid leaks and duplicate handlers.

Related explainers

javascript
const express = require('express');
const { createProxyMiddleware, fixRequestBody } = require('http-proxy-middleware');
 
const router = express.Router();

Building an API gateway proxy in Express

reverse-proxy middleware api-gateway
Intermediate 6 steps
javascript
import { useCallback, useRef, useState } from 'react';
 
export function ColorPicker({ initialColor = '#3b82f6', onCommit }) {
  const [committed, setCommitted] = useState(initialColor);

A validated color picker in React

uncontrolled-inputs refs validation
Intermediate 7 steps
javascript
import { NextResponse } from 'next/server';
import { Redis } from '@upstash/redis';
 
const redis = Redis.fromEnv();

Sliding-window rate limiting in a Next.js route

rate-limiting redis sorted-set
Advanced 8 steps
javascript
const MAX_FILE_SIZE = 5 * 1024 * 1024;
 
const ALLOWED_TYPES = {
  'image/jpeg': ['jpg', 'jpeg'],

Validating file uploads by content, not just claims

input-validation security magic-bytes
Intermediate 7 steps
javascript
function zip(keys, values) {
  if (keys.length !== values.length) {
    throw new RangeError('zip expects arrays of equal length');
  }

Three ways to zip arrays in JavaScript

arrays higher-order-functions pairing
Intermediate 6 steps
javascript
import { useCallback, useEffect, useState } from 'react';
 
export function useLocalStorage(key, initialValue) {
  const readValue = useCallback(() => {

How a useLocalStorage hook syncs state in React

custom hooks localstorage state persistence
Intermediate 8 steps

Share this explainer

Here's the card — post it anywhere.

A React hook that refetches on tab focus — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code