javascript
45 lines · 9 steps
A React hook that persists form state to localStorage
usePersistentForm keeps form values in localStorage with debounced writes so a refresh never loses what the user typed.
Explained by
highlit
1import { useState, useEffect, useCallback, useRef } from "react";
2
3export function usePersistentForm(storageKey, initialValues) {
4 const [values, setValues] = useState(() => {
5 try {
6 const stored = window.localStorage.getItem(storageKey);
7 return stored ? { ...initialValues, ...JSON.parse(stored) } : initialValues;
8 } catch {
9 return initialValues;
10 }
11 });
12
13 const saveTimer = useRef(null);
14
15 useEffect(() => {
16 clearTimeout(saveTimer.current);
17 saveTimer.current = setTimeout(() => {
18 try {
19 window.localStorage.setItem(storageKey, JSON.stringify(values));
20 } catch {
21 /* quota exceeded or storage unavailable */
22 }
23 }, 400);
24 return () => clearTimeout(saveTimer.current);
25 }, [storageKey, values]);
26
27 const handleChange = useCallback((event) => {
28 const { name, type, value, checked } = event.target;
29 setValues((prev) => ({
30 ...prev,
31 [name]: type === "checkbox" ? checked : value,
32 }));
33 }, []);
34
35 const setField = useCallback((name, value) => {
36 setValues((prev) => ({ ...prev, [name]: value }));
37 }, []);
38
39 const reset = useCallback(() => {
40 window.localStorage.removeItem(storageKey);
41 setValues(initialValues);
42 }, [storageKey, initialValues]);
43
44 return { values, handleChange, setField, reset };
45}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A lazy initializer in useState lets you hydrate from storage exactly once without re-reading on every render.
- 2Debouncing writes through a ref'd timer avoids hammering localStorage on every keystroke.
- 3Wrapping storage access in try/catch keeps the hook resilient when storage is full or unavailable.
Related explainers
javascript
'use server' import { revalidatePath } from 'next/cache' import { redirect } from 'next/navigation'
How a Next.js Server Action updates a post
server-actions
authorization
validation
Intermediate
7 steps
javascript
const express = require('express'); const v1 = express.Router();
Versioning an API with Express Routers
api versioning
routing
modularity
Intermediate
10 steps
javascript
const RATE_LIMIT = 100; const WINDOW_MS = 60 * 1000; const BLOCK_MS = 5 * 60 * 1000;
Building a rate-limiting middleware in Express
rate-limiting
middleware
closures
Intermediate
9 steps
javascript
const transitions = { cart: { checkout: 'shipping' }, shipping: { submitAddress: 'payment', back: 'cart' }, payment: { submitPayment: 'review', back: 'shipping' },
A finite state machine for checkout flow
state-machine
event-driven
data-driven-design
Intermediate
7 steps
typescript
import { Component } from '@angular/core'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { AsyncPipe } from '@angular/common';
Reactive type-ahead search in Angular
rxjs
reactive-forms
debounce
Intermediate
9 steps
javascript
function groupBy(items, keySelector) { const resolveKey = typeof keySelector === 'function' ? keySelector : (item) => item[keySelector];
Building a flexible groupBy in JavaScript
higher-order-functions
reduce
data-transformation
Intermediate
6 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-react-hook-that-persists-form-state-to-localstorage-explained-javascript-a662/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.