useHooks.iov4.1.2
DocsBlogGitHub

Mastering Persistent State: The useLocalStorage Hook

Learn how to seamlessly persist React state with the useLocalStorage hook - featuring automatic JSON serialization, error handling, and functional updates.

By usehooks.io

use-local-storagestatepersistencelocalStorage

Persisting user data across browser sessions is a common requirement in modern web applications. Whether you're saving user preferences, form data, or application settings, localStorage provides a simple way to store data locally. The useLocalStorage hook takes this a step further by seamlessly integrating localStorage with React state management.

What is useLocalStorage?

The useLocalStorage hook is a custom React hook that bridges the gap between React state and browser localStorage. It provides a useState-like API while automatically synchronizing your component state with localStorage, complete with JSON serialization and error handling.

Key Features

🔄 Automatic Synchronization

The hook automatically syncs your React state with localStorage, ensuring data persists across browser sessions and page refreshes.

📦 JSON Serialization

Built-in JSON serialization and deserialization means you can store complex objects, arrays, and primitives without manual conversion.

🛡️ Error Handling

Gracefully handles localStorage errors, such as when localStorage is unavailable (private browsing mode) or when storage quota is exceeded.

🔧 Functional Updates

Supports functional updates just like useState, allowing you to update state based on the previous value.

🎯 Type Safe

Fully typed with TypeScript generics, providing complete type safety for your stored data.

The Implementation

Let's examine how this hook works under the hood:

1"use client";
2
3import { useState } from "react";
4
5type SetValue<T> = T | ((val: T) => T);
6
7export function useLocalStorage<T>(
8  key: string,
9  initialValue: T
10): [T, (value: SetValue<T>) => void] {
11  const [storedValue, setStoredValue] = useState<T>(() => {
12    try {
13      const item = window.localStorage.getItem(key);
14      return item ? JSON.parse(item) : initialValue;
15    } catch (error) {
16      console.log(error);
17      return initialValue;
18    }
19  });
20
21  const setValue = (value: SetValue<T>) => {
22    try {
23      const valueToStore =
24        value instanceof Function ? value(storedValue) : value;
25      setStoredValue(valueToStore);
26      window.localStorage.setItem(key, JSON.stringify(valueToStore));
27    } catch (error) {
28      console.log(error);
29    }
30  };
31
32  return [storedValue, setValue];
33}
34

Basic Usage

The hook follows the same pattern as useState, making it intuitive to use:

1import { useLocalStorage } from "@usehooks-io/hooks";
2
3function UserProfile() {
4  const [name, setName] = useLocalStorage("user-name", "Anonymous");
5
6  return (
7    <div>
8      <p>Hello, {name}!</p>
9      <input
10        value={name}
11        onChange={(e) => setName(e.target.value)}
12        placeholder="Enter your name"
13      />
14    </div>
15  );
16}
17

Advanced Usage

Storing Complex Objects

The hook excels at storing complex data structures:

1import { useLocalStorage } from "@usehooks-io/hooks";
2
3interface UserSettings {
4  theme: "light" | "dark";
5  notifications: boolean;
6  language: string;
7}
8
9function SettingsPanel() {
10  const [settings, setSettings] = useLocalStorage<UserSettings>(
11    "user-settings",
12    {
13      theme: "light",
14      notifications: true,
15      language: "en",
16    }
17  );
18
19  const toggleTheme = () => {
20    setSettings((prev) => ({
21      ...prev,
22      theme: prev.theme === "light" ? "dark" : "light",
23    }));
24  };
25
26  const toggleNotifications = () => {
27    setSettings((prev) => ({
28      ...prev,
29      notifications: !prev.notifications,
30    }));
31  };
32
33  return (
34    <div>
35      <h2>Settings</h2>
36      <div>
37        <label>
38          <input
39            type="checkbox"
40            checked={settings.theme === "dark"}
41            onChange={toggleTheme}
42          />
43          Dark Mode
44        </label>
45      </div>
46      <div>
47        <label>
48          <input
49            type="checkbox"
50            checked={settings.notifications}
51            onChange={toggleNotifications}
52          />
53          Enable Notifications
54        </label>
55      </div>
56      <div>
57        <select
58          value={settings.language}
59          onChange={(e) =>
60            setSettings((prev) => ({ ...prev, language: e.target.value }))
61          }
62        >
63          <option value="en">English</option>
64          <option value="es">Spanish</option>
65          <option value="fr">French</option>
66        </select>
67      </div>
68    </div>
69  );
70}
71

Shopping Cart Persistence

1import { useLocalStorage } from "@usehooks-io/hooks";
2
3interface CartItem {
4  id: number;
5  name: string;
6  price: number;
7  quantity: number;
8}
9
10function ShoppingCart() {
11  const [cartItems, setCartItems] = useLocalStorage<CartItem[]>(
12    "shopping-cart",
13    []
14  );
15
16  const addToCart = (product: Omit<CartItem, "quantity">) => {
17    setCartItems((prev) => {
18      const existingItem = prev.find((item) => item.id === product.id);
19      if (existingItem) {
20        return prev.map((item) =>
21          item.id === product.id
22            ? { ...item, quantity: item.quantity + 1 }
23            : item
24        );
25      }
26      return [...prev, { ...product, quantity: 1 }];
27    });
28  };
29
30  const removeFromCart = (id: number) => {
31    setCartItems((prev) => prev.filter((item) => item.id !== id));
32  };
33
34  const clearCart = () => {
35    setCartItems([]);
36  };
37
38  const total = cartItems.reduce(
39    (sum, item) => sum + item.price * item.quantity,
40    0
41  );
42
43  return (
44    <div>
45      <h2>Shopping Cart</h2>
46      {cartItems.length === 0 ? (
47        <p>Your cart is empty</p>
48      ) : (
49        <>
50          {cartItems.map((item) => (
51            <div key={item.id} className="cart-item">
52              <span>{item.name}</span>
53              <span>Qty: {item.quantity}</span>
54              <span>${(item.price * item.quantity).toFixed(2)}</span>
55              <button onClick={() => removeFromCart(item.id)}>Remove</button>
56            </div>
57          ))}
58          <div className="cart-total">
59            <strong>Total: ${total.toFixed(2)}</strong>
60          </div>
61          <button onClick={clearCart}>Clear Cart</button>
62        </>
63      )}
64    </div>
65  );
66}
67

Form Data Persistence

Perfect for saving draft content or form progress:

1import { useLocalStorage } from "@usehooks-io/hooks";
2
3interface FormData {
4  title: string;
5  content: string;
6  category: string;
7}
8
9function BlogPostEditor() {
10  const [formData, setFormData] = useLocalStorage<FormData>("blog-draft", {
11    title: "",
12    content: "",
13    category: "general",
14  });
15
16  const updateField = (field: keyof FormData, value: string) => {
17    setFormData((prev) => ({ ...prev, [field]: value }));
18  };
19
20  const clearDraft = () => {
21    setFormData({ title: "", content: "", category: "general" });
22  };
23
24  return (
25    <form>
26      <div>
27        <label>Title:</label>
28        <input
29          type="text"
30          value={formData.title}
31          onChange={(e) => updateField("title", e.target.value)}
32          placeholder="Enter post title"
33        />
34      </div>
35      <div>
36        <label>Category:</label>
37        <select
38          value={formData.category}
39          onChange={(e) => updateField("category", e.target.value)}
40        >
41          <option value="general">General</option>
42          <option value="tech">Technology</option>
43          <option value="lifestyle">Lifestyle</option>
44        </select>
45      </div>
46      <div>
47        <label>Content:</label>
48        <textarea
49          value={formData.content}
50          onChange={(e) => updateField("content", e.target.value)}
51          placeholder="Write your post content..."
52          rows={10}
53        />
54      </div>
55      <div>
56        <button type="submit">Publish</button>
57        <button type="button" onClick={clearDraft}>
58          Clear Draft
59        </button>
60      </div>
61      {(formData.title || formData.content) && (
62        <p>
63          <em>Draft automatically saved</em>
64        </p>
65      )}
66    </form>
67  );
68}
69

Real-World Applications

The useLocalStorage hook is invaluable in many scenarios:

  1. User Preferences: Theme settings, language preferences, layout configurations
  2. Form Persistence: Auto-saving form data, draft content, multi-step form progress
  3. Shopping Carts: Persisting cart items across sessions
  4. Application State: Recently viewed items, search history, filter preferences
  5. Game Progress: High scores, game settings, save states
  6. Authentication: Remember user preferences (not sensitive data!)

Why Choose useLocalStorage?

✅ Seamless Integration

Works exactly like useState but with automatic persistence - no learning curve required.

✅ Robust Error Handling

Gracefully handles localStorage limitations and errors without breaking your app.

✅ Performance Optimized

Only updates localStorage when state actually changes, avoiding unnecessary writes.

✅ Type Safety

Full TypeScript support ensures your stored data maintains its expected structure.

✅ Zero Configuration

Works out of the box with sensible defaults and automatic JSON handling.

✅ Functional Updates

Supports the same functional update pattern as useState for complex state updates.

Best Practices

  1. Choose meaningful keys: Use descriptive localStorage keys that won't conflict with other applications.

  2. Handle sensitive data carefully: Never store passwords, tokens, or other sensitive information in localStorage.

  3. Consider data size: localStorage has size limits (usually 5-10MB). For large datasets, consider other storage solutions.

  4. Provide good defaults: Always provide sensible initial values that make your app functional even when localStorage is empty.

  5. Test edge cases: Test your app in private browsing mode where localStorage might be restricted.

  6. Clean up when needed: Consider implementing cleanup logic for old or unused localStorage entries.

Browser Compatibility

The hook works in all modern browsers that support localStorage:

  • Chrome 4+
  • Firefox 3.5+
  • Safari 4+
  • Internet Explorer 8+
  • Edge (all versions)

The hook gracefully degrades when localStorage is unavailable, falling back to regular state management.

Conclusion

The useLocalStorage hook bridges the gap between React state management and browser persistence, providing a powerful yet simple solution for maintaining user data across sessions. Its automatic JSON handling, robust error management, and useState-compatible API make it an essential tool for modern React applications.

Whether you're building user preference systems, shopping carts, or form auto-save functionality, useLocalStorage provides the reliability and ease of use you need. Its seamless integration with React's state management patterns means you can add persistence to your applications without changing how you think about state.

Try incorporating useLocalStorage into your next React project and experience the power of effortless state persistence!