← FC Coder · HomePhase 02 · Lesson 22 · 60 min
Lesson22
Phase Two · Fantasy Builder · Matchday 22 · Coach Speaks Up

规则引擎
教练大声说话

Today's 3 Jobs · 今天这三件事
  1. 01
    写 getErrors(squad) · 两条规则:11 人 + 1 GK
    纯函数 · 集中校验
  2. 02
    errors / isComplete 派生 · 不存 state
    能算的别存
  3. 03
    🌟 红字 + 灰按钮 → 全过变金按钮
    教练大声说话

21 课教练手里那张大战术板 · 静悄悄不收第 12 个。今天教练吼出来:不满 11 红字"还差 X 个" · 没门将红字"必须有 1 个 GK" · 保存按钮到合规为止才变心法:`errors` / `isComplete` 全部从 squad 派生 · 0 个新 useState。能算的别存

Concept · 第 1 个新概念

派生状态 · 能算的别存

俱乐部财务:翻工资单加 vs 抄一份贴墙。

Chalk Board · 派生 vs 存
// 派生 3 个值 · 0 个新 useState
const errors = getErrors(squad);
const isComplete = errors.length === 0;
存(错)
const [hasGK, setHasGK] = useState(false)
派生(对)
const hasGK = squad.some(p => p.position === 'GK')
好处
0 个新 useState · 永远和 squad 一致

心法:只有"用户输入 / 异步取来 / 时间相关"才 useState。其他能 from-state-derive 全部别。

Concept · 第 2 个新概念

校验逻辑 · 一函数 → errors 数组

规则集中在 getErrors —— 加规则就改这一处。

Chalk Board · getErrors
function getErrors(squad: Player[]): string[] {
const errors: string[] = [];
if (squad.length < 11) errors.push(...);
if (!squad.some(p => p.position === 'GK')) errors.push(...);
return errors;
}
规则 1
squad.length < 11 → 还差 X 个
规则 2
!squad.some(p => p.position === 'GK') → 必须 1 GK
返回
errors: string[]

空 errors = 合规 · 多 errors 同时显示。plan 锁 2 条 · 想加先改 plan。

Roster · 今天 3 个新工具

派生 · 校验 · 禁用按钮

抄着用 · Step 8 灰→金切换是今天的「哇」。

  1. 01
    const x = f(state)
    派生状态 —— 能算的别存。errors / isComplete / hasGK 全部从 squad 算。
    0 个新 useState。React 心法第 1 条。
  2. 02
    getErrors(squad): string[]
    校验逻辑 —— 一组规则 → errors 数组。
    规则在一处 · 加规则就改 getErrors。今天只 2 条。
  3. 03
    <button disabled={!ok}>
    禁用按钮 —— HTML 内置 + 条件 className。
    按钮显示但不能点 · 比"不显示"友好(看得见按钮在等什么)。
Sandbox · 在课程页里先玩一下

迷你阵容板 + 规则(5 人 + 必须 1 GK)

凑 5 人 · 选一个门将 · 看保存按钮变金。

迷你阵容板 · 规则版

0 / 5

可选 · 11

哈兰德

ST · 评分 91

挪威 · 曼城

福登

LW · 评分 87

英国 · 曼城

罗德里

CM · 评分 90

西班牙 · 曼城

德布劳内

CM · 评分 88

比利时 · 曼城

阿坎吉

CB · 评分 84

瑞士 · 曼城

埃德森

GK · 评分 88

巴西 · 曼城

多库

LW · 评分 82

比利时 · 曼城

贝尔纳多

CM · 评分 86

葡萄牙 · 曼城

阿克

CB · 评分 81

荷兰 · 曼城

迪亚斯

CB · 评分 85

葡萄牙 · 曼城

格瓦迪奥尔

CB · 评分 84

克罗地亚 · 曼城

阵容

  • ⚠️ 还差 5 个 —— 一共要 5 人
  • ⚠️ 必须有 1 个门将(GK)

左边点 5 个进来。点右边的卡 = 踢出阵容。

提示:state 在 FormationSandbox 顶部的 useState<Player[]> · errors / isComplete 全部派生(无 useState)。

Half 2 · 在屏幕上

写 getErrors · 装红字 · 装按钮 · 切灰金

做完一步就点 ✓。Step 8 灰→金那一下是今天的「哇」。

01写 getErrors + 派生 isComplete
01Min

Cursor + pnpm dev + 打开 /fantasy

老三件套。看到 21 课样子 · 左右两栏 · 0/11。
02Min

在 FantasyPage 外面写 function getErrors(squad: Player[]): string[]

纯函数 · 不依赖组件 state。两条规则:if (squad.length < 11) errors.push(`还差 ${11 - squad.length} 个 ...`)。if (!squad.some(p => p.position === 'GK')) errors.push('必须有 1 个门将(GK)')。return errors。
03Min

组件内 const errors = getErrors(squad); const isComplete = errors.length === 0

useState 下面、addPlayer 上面。两行 · 0 个新 useState。这就是派生。
02红字清单 + 保存按钮
04Min

右栏顶加 errors.map → 红字 ⚠️ 列表

{errors.length > 0 && (<ul className="mb-4 space-y-1 text-sm text-red-400">{errors.map((err, i) => <li key={i}>⚠️ {err}</li>)}</ul>)}。保存。右栏顶看到 2 行红字。
05Min

右栏底加 <button disabled={!isComplete}>...</button>

金 / 灰两套 className 切换。{isComplete ? '✅ 保存阵容' : '阵容未合规'}。onClick alert 一下('阵容已保存!11 人 · 1 个门将')。Phase 23 才真存。
03🌟 灰 → 金 切换
06Min

凑 11 个非 GK 球员 · 看红字只剩 1 条

故意不选门将 · 凑到 11/11。'还差 X 个' 消失 · '必须有 1 个门将' 还在。规则互相独立。
07Min

踢一个非 GK · 加埃德森(GK · 91)

右栏点踢出 · 左边找埃德森(曼城门将 · 第一行那个 91 旁边)· 加进来。红字全部消失。
08Min

🌟 按钮变金 · 点 → alert 弹出

今天最爽那一下。再踢门将 → 按钮立刻灰回去 + 字'阵容未合规'。
04顺手修 19 · 截图
09Min

顺手修 lesson 19 sandbox 的预览卡

PlayerCard 21 课改受控后 · 19 课预览点不动。在 19 课 page.tsx 加一个 Lesson19PreviewCard 子组件 · 内部 useState(false) · 把 <PlayerCard player={players[0]} /> 替成 <Lesson19PreviewCard />。访问 /lessons/19-usestate 哈兰德卡能点了。
10Min

看 fantasy/page.tsx · 全文 useState 仍只 1 个

errors / isComplete / hasGK 全部派生 · 0 个新 useState。这是 React 心法的「小考核」。
11Min

口述第 3 条规则 · 但不要加

想象'必须有曼城球员':if (!squad.some(p => p.club === '曼城')) errors.push(...)。口述完别合并 · Plan 003 锁了 2 条。这就是工程素养。
12Min

📸 截图战利品

Mac Shift + Cmd + 4 框'红字 + 灰按钮' 或 '金按钮亮起' 那一刻。存 2026-XX-XX-教练大声说话.png · Phase 2 第七张战利品。
Half Time · 中场 · 讲给爸爸听

4 题 · 重点:能算的别存

第 2 题答得出 = 派生概念真懂了。

01什么是「派生状态」?用「俱乐部财务」比喻。Hint ↓

算 vs 存。每次问总工资翻一遍工资单加起来 = 永远准。抄一份贴墙上 = 忘改就错。

02errors 数组为什么不是 useState?Hint ↓

它从 squad 算 · squad 一变 errors 自动新算 · 不用同步。React 心法第 1 条:能算的别存。

03<button disabled={!isComplete}> 是什么意思?为什么不直接不渲染按钮?Hint ↓

按钮显示但不能点 · 比不显示友好。用户看得见按钮在等什么。disabled 是 HTML 内置。

04如果你想加第 3 条规则「必须有曼城球员」· 在哪改?Hint ↓

只改 getErrors —— 加一个 if。规则集中在一处。今天忍住不加 · 因为 plan 锁 2 条。

Player Rating · 本课温度计

给「教练大声说话」打个分

说真话。爸爸会看到。

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

教练终于会说话了 —— 阵容不合规红字喊出 · 合规金按钮亮起。

还有 12 步没打勾。Step 8 灰→金那一下今天就过 —— 剩下改天补。