← FC Coder · HomePhase 02 · Lesson 23 · 60 min
Lesson23
Phase Two · Fantasy Builder · Matchday 23 · Long Memory

localStorage 阵容保存
刷新不丢

Today's 3 Jobs · 今天这三件事
  1. 01
    lib/progress.ts 加 useLocalStorage<T> 自定义 hook
    useState + useEffect 包一处
  2. 02
    fantasy/page.tsx 用 useLocalStorage 替 useState
    一行调用 · 全项目复用
  3. 03
    🌟 F5 刷新 · 阵容还在
    教练的球员手册

22 课教练大声说话 —— 但刷新就丢。今天上长记忆 —— `localStorage`(浏览器小本子) + `useEffect` 第一次正经见,但包在 `useLocalStorage` hook 里,孩子一行调用搞定。F5 后阵容还在 = 今天的「哇」。

Concept · 第 1 个新概念

短记忆 vs 长记忆

useState 在 React 内存 · localStorage 在浏览器硬盘。

Chalk Board · 记忆类型对照
localStorage.setItem('squad', JSON.stringify(squad));
const raw = localStorage.getItem('squad');
const squad = raw ? JSON.parse(raw) : [];
useState
React 内存 · 刷新没了
localStorage
浏览器硬盘 · 刷新还在
服务器
Phase 3+ · 跨设备永远在

localStorage 只装字符串 → JSON.stringify(写)+ JSON.parse(读)。教练的球员手册。

Concept · 第 2 个新概念

useEffect · 同步外部世界

dep 变 · 跑一次「和 React 之外对话」。

Chalk Board · useEffect
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value, hydrated]);
签名
useEffect(fn, [deps])
今天用途
squad 变 → 写 localStorage
不教
派生用 effect(反 React 官方共识)

我们包成 useLocalStorage hook · 全项目一行调用 · 隐藏 effect 细节。

Roster · 今天 3 个新工具

localStorage · useEffect · 自定义 hook

包成一个 useLocalStorage —— 全项目一行复用。

  1. 01
    localStorage
    浏览器小本子 —— 刷新 / 关浏览器都还在。
    setItem / getItem / removeItem 三件套。只装字符串 → JSON.stringify/parse。
  2. 02
    useEffect(() => {...}, [dep])
    同步外部世界 —— dep 变 · 跑一次"和 React 之外对话"。
    今天唯一用途:写 localStorage。不教派生用 effect(反共识)。
  3. 03
    useLocalStorage<T>(key, initial)
    自定义 hook —— useState + useEffect 包一起 · 一行调用。
    其他课要存东西 · 抄一行就好。Phase 2 → 3 关键沉淀。
Half 2 · 在屏幕上

写 hook · 接入 · F5 不丢 · 加清空

做完一步就点 ✓。Step 5 F5 那一下今天的「哇」。

01写 useLocalStorage hook
01Min

Cursor + pnpm dev + 打开 lib/progress.ts

老三件套。看到已有的 useChildName / useLessonProgress。
02Min

末尾加 export function useLocalStorage<T>(key, initial)

里面 2 个 useState(value / hydrated)+ 2 个 useEffect:① mount 读 localStorage ② value 变写 localStorage。try/catch 兜底。return [value, setValue, hydrated] as const。
02接入 fantasy/page.tsx
03Min

fantasy/page.tsx · useState → useLocalStorage

import { useLocalStorage } from '@/lib/progress'。const [squad, setSquad, hydrated] = useLocalStorage<Player[]>('fc-coder:squad', [])。删 useState import(如果只它一个)。
04Min

打开 DevTools → Application → localStorage

Cmd+Opt+I → Application 标签 → Storage → Local Storage → localhost:3000。选 5 个球员 · 看到 fc-coder:squad 实时更新。
03🌟 F5 不丢 · 关浏览器不丢
05Min

🌟 F5 刷新 · 阵容还在!

今天的「哇」。Cmd+R 或 F5。你选的 5 个球员 · 一个不少。
06Min

关浏览器标签 · 重开 · 阵容还在

更狠的测试。Localhost session 关掉重开 · 阵容仍持久。
07Min

DevTools 手改 localStorage 的值 · 看页面

双击 fc-coder:squad 的值 · 改一个球员名(字符串里)· 回车。页面不会自动重画(单向 React → localStorage)· 但 F5 后读出改后的数据。这是边界 · 不修。
04清空按钮 · 截图
08Min

加「清空阵容」按钮 · setSquad([])

右栏保存按钮下面。{squad.length > 0 && <button onClick={() => setSquad([])}>清空阵容</button>}。点 · 阵容空 · localStorage 也清(因为 useLocalStorage 同步写空数组)。
09Min

鼠标停在 useLocalStorage 上 · 看 Cursor 弹类型

应该看到 [Player[], setSquad: ..., hydrated: boolean]。第 3 项 hydrated 今天先不显式用 · Phase 3 SSR 那节会用来挡 UI 闪烁。
10Min

看 fantasy/page.tsx · 0 个 useState · 0 个 useEffect

全部包进 useLocalStorage 了。复杂细节藏一个地方 · 用的人只 1 行。
11Min

想象第二个用法 · 比如阵容名字

const [name, setName] = useLocalStorage<string>('fc-coder:formation-name', '我的梦之队')。口述就好 —— 延伸题 ⭐️ 真做。
12Min

📸 截图战利品

存 2026-XX-XX-刷新不丢.png · Phase 2 第八张战利品。两张对比:选好 5 人 + F5 之后。
Half Time · 中场 · 讲给爸爸听

4 题 · 重点:为什么包成 hook

第 4 题答得出 = 自定义 hook 心智到了。

01useState 和 localStorage 差在哪?用「短记忆 vs 长记忆」讲。Hint ↓

useState 在 React 内存 · 刷新没。localStorage 在浏览器硬盘 · 刷新还在。教练的球员手册。

02localStorage.setItem 为什么要 JSON.stringify(value)?Hint ↓

localStorage 只装字符串。数组要先变字符串(stringify)· 读回来再 parse 变回数组。

03useEffect 是干什么的?Hint ↓

和 React 之外的世界对话 —— 写 localStorage / 拉网络 / 监听键盘。今天唯一用途:同步 squad → localStorage。

04为什么把 useState + useEffect 包成 useLocalStorage?Hint ↓

复用 + 隐藏复杂度。其他课要存东西 · 抄一行就好。Phase 2 → 3 关键沉淀。

Player Rating · 本课温度计

给「刷新不丢」打个分

说真话。爸爸会看到。

今天难度Difficulty
0
今天开心Fun
0
Final Whistle · 终场哨

教练终于有了球员手册 —— 关浏览器都还在。

还有 12 步没打勾。Step 5 F5 那一下今天就过 —— 剩下改天补。