第二章练习答案
每题代码分别替换 src/App.jsx。断言失败会直接抛错,避免 console.assert 只打印消息后仍让验证看似成功。
答案 01:比分回合队列
jsx
import { useState } from 'react'
export default function App() {
const [score, setScore] = useState(10)
const [rounds, setRounds] = useState([])
function addThree() {
setScore(value => value + 1)
setScore(value => value + 1)
setScore(value => value + 1)
}
function applyPenalty() {
setScore(20)
setScore(value => value + 2)
}
function recordRound() {
setRounds(current => [...current, score])
setScore(value => value + 1)
}
return (
<main>
<h1>比分:{score}</h1>
<button onClick={addThree}>罚球三中</button>
<button onClick={applyPenalty}>判罚改为 20 后加 2</button>
<button onClick={recordRound}>记录本回合</button>
<ol>{rounds.map((value, index) => <li key={index}>{value}</li>)}</ol>
</main>
)
}从 10 点击“罚球三中”得到 13:队列是 10→11→12→13。判罚按钮不依赖旧快照,先替换为 20,更新函数再收到 20 并得到 22。记录按钮中的 score 是该次渲染快照,所以从 22 点击后,日志追加 22,下一次界面显示 23。
答案 02:库存批次更新函数
jsx
import { useState } from 'react'
const initialItems = [
{ id: 'tea', name: '茶叶', batches: [
{ id: 'T-1', quantity: 4 },
{ id: 'T-2', quantity: 7 },
] },
{ id: 'cup', name: '杯子', batches: [{ id: 'C-1', quantity: 3 }] },
]
export function updateBatch(items, itemId, batchId, quantity) {
if (!Number.isInteger(quantity) || quantity < 0) return items
const itemIndex = items.findIndex(item => item.id === itemId)
if (itemIndex === -1) return items
const batchIndex = items[itemIndex].batches.findIndex(batch => batch.id === batchId)
if (batchIndex === -1) return items
const oldItem = items[itemIndex]
const newBatches = oldItem.batches.map(batch => (
batch.id === batchId ? { ...batch, quantity } : batch
))
return items.map(item => (
item.id === itemId ? { ...item, batches: newBatches } : item
))
}
function check(condition, message) {
if (!condition) throw new Error(message)
}
const changed = updateBatch(initialItems, 'tea', 'T-1', 9)
check(changed !== initialItems, '外层数组应更新')
check(changed[0] !== initialItems[0], '目标商品应更新')
check(changed[0].batches[0] !== initialItems[0].batches[0], '目标批次应更新')
check(changed[0].batches[1] === initialItems[0].batches[1], '其他批次应复用')
check(changed[1] === initialItems[1], '其他商品应复用')
check(initialItems[0].batches[0].quantity === 4, '旧输入不可被修改')
check(updateBatch(initialItems, 'missing', 'T-1', 9) === initialItems, '未找到应原样返回')
check(updateBatch(initialItems, 'tea', 'missing', 9) === initialItems, '未找到批次应原样返回')
check(updateBatch(initialItems, 'tea', 'T-1', -1) === initialItems, '非法数量应原样返回')
check(updateBatch(initialItems, 'tea', 'T-1', 1.5) === initialItems, '小数应原样返回')
check(updateBatch(initialItems, 'tea', 'T-1', NaN) === initialItems, 'NaN 应原样返回')
check(updateBatch(initialItems, 'tea', 'T-1', 0)[0].batches[0].quantity === 0, '零库存是合法更新')
export default function App() {
const [items, setItems] = useState(initialItems)
return (
<main>
<h1>批次库存</h1>
{items.map(item => (
<section key={item.id}>
<h2>{item.name}</h2>
{item.batches.map(batch => (
<label key={batch.id}>
{batch.id}
<input
type="number"
min="0"
step="1"
value={batch.quantity}
onChange={event => {
const quantity = Number(event.target.value)
setItems(current => updateBatch(current, item.id, batch.id, quantity))
}}
/>
</label>
))}
</section>
))}
</main>
)
}先确认商品和批次存在,才能判断是否需要新引用;否则 map 即使找不到目标也会无意义地产生新数组。断言分别覆盖外层、变化路径、未变化分支、旧值和失败分支,能排除“深拷贝一切”和“只复制外层”两类错误实现。
答案 03:按项目隔离的搜索草稿
jsx
import { useState } from 'react'
const projects = [
{ id: 'docs', name: '文档站', items: ['React 导读', '链接检查', '部署说明'] },
{ id: 'shop', name: '商店', items: ['商品页', '购物车', '结算页'] },
]
function SearchPanel({ project, query, onQueryChange }) {
const normalized = query.trim().toLocaleLowerCase()
const results = project.items.filter(item => (
item.toLocaleLowerCase().includes(normalized)
))
return (
<section>
<h2>{project.name}</h2>
<input
aria-label="搜索"
value={query}
onChange={event => onQueryChange(event.target.value)}
/>
{results.length === 0 ? <p>无匹配项</p> : (
<ul>{results.map(item => <li key={item}>{item}</li>)}</ul>
)}
</section>
)
}
export default function App() {
const [selectedId, setSelectedId] = useState(projects[0].id)
const [drafts, setDrafts] = useState({})
const selected = projects.find(project => project.id === selectedId)
const query = drafts[selectedId] ?? ''
function changeQuery(value) {
setDrafts(current => ({ ...current, [selectedId]: value }))
}
return (
<main>
<h1>项目搜索</h1>
{projects.map(project => (
<button key={project.id} onClick={() => setSelectedId(project.id)}>
{project.name}
</button>
))}
<SearchPanel project={selected} query={query} onQueryChange={changeQuery} />
</main>
)
}验收:在文档站输入“链接”,切到商店输入“页”,再切回时分别恢复。results 每次由 project.items + query 计算,不存在需要同步的第三份 state;selected 也从稳定 id 查找,所以项目对象更新不会留下旧副本。
返回第二章导读。