Langkau ke kandungan
WANCODE
EN

Hook useLocalStorage (React)

Pengganti terus untuk useState yang mengingati nilainya selepas reload dan kekal selari antara tab.

useLocalStorage.js
import { useEffect, useRef, useState } from "react";

/**
 * Like useState, but the value is saved in localStorage and
 * restored on the next visit. Values must be JSON-serialisable.
 */
export function useLocalStorage(key, initialValue) {
  const initialRef = useRef(initialValue);

  const [value, setValue] = useState(() => {
    try {
      const stored = window.localStorage.getItem(key);
      return stored !== null ? JSON.parse(stored) : initialValue;
    } catch {
      // No window (server render), storage blocked, or invalid JSON
      return initialValue;
    }
  });

  // Save whenever the value changes
  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch {
      // Storage full or disabled — keep working in memory
    }
  }, [key, value]);

  // Stay in sync when another tab changes the same key
  useEffect(() => {
    function onStorage(event) {
      if (event.storageArea !== window.localStorage || event.key !== key) return;
      try {
        setValue(event.newValue !== null ? JSON.parse(event.newValue) : initialRef.current);
      } catch {
        // Ignore values we can't parse
      }
    }
    window.addEventListener("storage", onStorage);
    return () => window.removeEventListener("storage", onStorage);
  }, [key]);

  return [value, setValue];
}

Custom hook hanyalah fungsi yang namanya bermula dengan use dan memanggil hook lain. Hook ini membalut useState dan menambah dua side effect: simpan ke localStorage bila nilai berubah, dan dengar perubahan daripada tab lain.

Baris demi baris

  • useState(() => …) guna lazy initialiser: fungsi itu hanya berjalan pada render pertama, jadi kita baca dan parse storan sekali sahaja, bukan pada setiap render.
  • localStorage hanya simpan string, jadi nilai melalui JSON.stringify semasa disimpan dan JSON.parse semasa dibaca. Sebab itu nilai mesti mesra JSON (tiada fungsi, Date akan jadi string).
  • Setiap panggilan storan dalam try/catch: storan boleh dimatikan, penuh (QuotaExceededError) atau mengandungi JSON tidak sah. Hook ini kembali kepada state biasa dalam memori dan tidak merosakkan component anda.
  • useEffect pertama berjalan selepas render setiap kali key atau value berubah, jadi proses simpan tak melambatkan render itu sendiri.
  • Pelayar mencetuskan event storage dalam tab lain bagi laman yang sama bila satu tab menulis. Effect kedua mendengarnya dan mengemas kini state, kemudian membuang listener dalam fungsi cleanup.
  • initialRef mengingati initialValue yang pertama, jadi menghantar array/objek literal baharu pada setiap render tidak menyebabkan listener dilanggan semula.

Ketahui hadnya

Jangan simpan rahsia atau token di sini — mana-mana skrip pada halaman boleh membaca localStorage. Dengan server rendering (contohnya Next.js), server sentiasa render initialValue; guna hook ini dalam client component sahaja. Menukar key semasa berjalan tidak memuat semula nilai tersimpan — remount component itu (beri prop key yang berbeza).

Cuba: buat projek React dalam Playground, tampal hook dan contoh penggunaan, kemudian tekan Run dua kali — nilai kekal kerana preview meniru localStorage bagi setiap projek.

Cara guna

JSX
import { useLocalStorage } from "./useLocalStorage.js";

export default function App() {
  const [name, setName] = useLocalStorage("profile-name", "");
  const [fontSize, setFontSize] = useLocalStorage("reader-font-size", 16);

  return (
    <main style={{ fontSize }}>
      <label>
        Your name{" "}
        <input value={name} onChange={(e) => setName(e.target.value)} />
      </label>
      <p>Hello, {name || "stranger"}! Reload — I still remember you.</p>
      <button type="button" onClick={() => setFontSize((s) => s + 2)}>A+</button>
      <button type="button" onClick={() => setFontSize((s) => Math.max(12, s - 2))}>A−</button>
    </main>
  );
}