Store data in the browser with localStorage and sessionStorage in a Next.js + TypeScript app — saving, reading, removing, typed JSON values, the difference between them, and the hydration gotcha.
Why: localStorage is a tiny key–value store built into the browser that survives reloads and restarts — good for a theme choice or a saved draft. Both keys and values are always strings, so getItem returns string | null.
// save
localStorage.setItem('theme', 'dark')
// read — the type is string | null (null if the key was never set)
const theme: string | null = localStorage.getItem('theme')
console.log(theme) // 'dark'
// remove one key, or wipe everything
localStorage.removeItem('theme')
localStorage.clear()Note: storage only holds strings, so you cannot save an object directly — turn it into a string with JSON.stringify when saving, and parse it back with JSON.parse when reading. Type the parsed result, and guard against null in case the key is missing.
type CartItem = { id: number; qty: number }
const cart: CartItem[] = [{ id: 1, qty: 2 }]
// save: object -> string
localStorage.setItem('cart', JSON.stringify(cart))
// read: string -> typed object (fall back to an empty array)
const saved: CartItem[] = JSON.parse(localStorage.getItem('cart') ?? '[]')
console.log(saved[0].qty) // 2When: they share the exact same API; the only difference is how long the data lives. localStorage stays until you delete it. sessionStorage is cleared when the tab closes — use it for things that should not outlive the visit, like a multi-step form in progress.
// persists across reloads and restarts
localStorage.setItem('language', 'en')
// gone when the tab is closed
sessionStorage.setItem('checkoutStep', '2')Note: localStorage exists only in the browser — there is no window on the server. In Next.js, reading it during render crashes or causes a hydration mismatch (the server HTML differs from the client). The fix: mark the component "use client" and read storage inside useEffect, which runs only in the browser, after the first render.
'use client'
import { useState, useEffect } from 'react'
export function useTheme() {
const [theme, setTheme] = useState<string>('light') // safe default for SSR
useEffect(() => {
// runs only in the browser, after hydration
const saved = localStorage.getItem('theme')
if (saved) setTheme(saved)
}, [])
return [theme, setTheme] as const // typed tuple
}