← FC Coder · HomePhase 02 · Lesson 23 · 60 min
Lesson23
Phase Two · Fantasy Builder · Matchday 23 · Long Memory
localStorage 阵容保存
刷新不丢
Today's 3 Jobs · 今天这三件事
- 01lib/progress.ts 加 useLocalStorage<T> 自定义 hookuseState + useEffect 包一处
- 02fantasy/page.tsx 用 useLocalStorage 替 useState一行调用 · 全项目复用
- 03🌟 F5 刷新 · 阵容还在教练的球员手册
22 课教练大声说话 —— 但刷新就丢。今天上长记忆 —— `localStorage`(浏览器小本子) + `useEffect` 第一次正经见,但包在 `useLocalStorage` hook 里,孩子一行调用搞定。F5 后阵容还在 = 今天的「哇」。
Concept · 第 1 个新概念
短记忆 vs 长记忆
useState 在 React 内存 · localStorage 在浏览器硬盘。
localStorage.setItem('squad', JSON.stringify(squad));
const raw = localStorage.getItem('squad');
const squad = raw ? JSON.parse(raw) : [];
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 之外对话」。
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value, hydrated]);
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 —— 全项目一行复用。
- 01localStorage浏览器小本子 —— 刷新 / 关浏览器都还在。setItem / getItem / removeItem 三件套。只装字符串 → JSON.stringify/parse。
- 02useEffect(() => {...}, [dep])同步外部世界 —— dep 变 · 跑一次"和 React 之外对话"。今天唯一用途:写 localStorage。不教派生用 effect(反共识)。
- 03useLocalStorage<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 那一下今天就过 —— 剩下改天补。