按类型分组渲染六张卡Rendering the six cards grouped by type
两层 map:外层 Object.keys 出三个类型,内层出每组的车。key 有个坑。Two nested maps: Object.keys gives the three types on the outside, the cabs of each group on the inside. The key needs care.
这一页有什么On this page5
- 用 Object.keys + 两层 map 把分组数据渲染出来Render grouped data with Object.keys and two nested maps
- 说清为什么分组顺序不用自己排Explain why you do not have to sort the group order yourself
- 写出 CabCard 的五个 data-testidWrite the five data-testid values of CabCard
- 知道为什么 ride.id 单独做 key 在历史列表里不安全Know why ride.id on its own is not a safe key in the history list
测试 2 一次查九个断言:一个容器、三个分组标题(有序)、五种卡片字段各 6 个。这一节把这九个断言一次性满足。分组顺序是送分题 —— 老实用 Object.keys 就对了,自己排序反而会错。Test 2 checks nine things at once: one container, three group headings in order, and 6 of each of the five card fields. This lesson satisfies all nine together. The group order is a free point: use Object.keys as it comes and you are right, while sorting it yourself makes it wrong.
cab-booking-context/src/data/data.json三组六辆车,键顺序 Sedan → SUV → LuxurySix cars in three groups, keyed in the order Sedan → SUV → Luxury
cab-booking-context/src/data/data.jsoncab-booking-context/src/components/CabOptions/CabOptions.jsx外层分组
提醒:源项目在磁盘上是做完的版本 —— 下面就是答案。想自己先写一遍的话,现在关上。Heads up: on disk this project is the finished version — what follows is the answer. Close this if you want to write it yourself first.
cab-booking-context/src/components/CabOptions/CabOptions.jsxcab-booking-context/src/components/CabOptions/CabCard.jsx五个字段都在这里
提醒:源项目在磁盘上是做完的版本 —— 下面就是答案。想自己先写一遍的话,现在关上。Heads up: on disk this project is the finished version — what follows is the answer. Close this if you want to write it yourself first.
cab-booking-context/src/components/CabOptions/CabCard.jsxObject.keys 加两层 mapObject.keys plus two nested maps
数据长什么样,代码就长什么样The shape of the code follows the shape of the data
一句话:data.json是「类型 → 车数组」的对象,所以外层遍历键、内层遍历值。
数据形状:
- 顶层是一个对象,三个键:
Sedan/SUV/Luxury - 每个键的值是一个数组,各 2 辆车
- 每辆车有
id/name/type/price/image
所以 3 × 2 = 6 张卡 —— 测试 2 里那五个 toHaveLength(6) 就是这么来的。
分组顺序是送分题:Object.keys() 返回的字符串键顺序, 在现代 JS 里就是它们被写进对象的顺序(这是 ES2015 起明确规定的,不是巧合)。data.json 里写的是 Sedan → SUV → Luxury, 所以 Object.keys(cabData) 出来就是这个顺序, 正好对上测试 2 的 toEqual。
反而是「我来排个序」会把它弄坏:.sort() 出来是字典序Luxury → SUV → Sedan,直接红。这一条的道理是:数据已经有意义的顺序时,别再加工。
会追问:「Object.keys 的顺序真的可靠吗?」—— 对字符串键可靠,插入顺序。 但纯数字键会被排到最前面并按数值升序({ "2": …, "10": …, "a": … }出来是 2, 10, a)。 这道题的键都是单词,撞不到; 但如果分组键是年份或编号,就得自己维护一个顺序数组。
In one line: data.json is a “type to array of cars” object, so the outer loop walks the keys and the inner loop walks the values.
The shape of the data:
- Top level is an object with three keys:
Sedan/SUV/Luxury - Each key holds an array of 2 cars
- Each car has
id/name/type/price/image
So 3 × 2 = 6 cards — that is where the five toHaveLength(6) assertions in test 2 come from.
The group order is a gift: the string keys returned by Object.keys() come back in the order they were written into the object in modern JS — specified since ES2015, not an accident. data.json lists Sedan → SUV → Luxury, so Object.keys(cabData) hands you exactly the order test 2’s toEqual wants.
“Let me sort it” is what breaks it: .sort() gives lexicographic order, Luxury → SUV → Sedan, which fails. The lesson: when the data already carries a meaningful order, do not touch it.
Follow-up: “Is Object.keys order actually reliable?” — for string keys, yes: insertion order. But purely numeric keys get hoisted to the front in ascending numeric order ({ "2": …, "10": …, "a": … } comes out as 2, 10, a). The keys here are words, so it never bites; but if your group keys were years or numeric ids you would have to keep your own order array.
cab-booking-context/src/components/CabOptions/CabOptions.jsxcab-booking-context/src/data/data.json五个字段,和 key 的那个坑The five fields, and the problem with the key
卡片里每个字段都有 testid;历史列表的 key 不能只用 idEvery field in the card has its own testid; the key in the history list cannot be the id alone
一句话:一张卡上五个data-testid:cab-card-img / -name / -type / -price / -select-button。
测试只数个数,不查内容 —— 五个断言都是 toHaveLength(6)。 但别因此偷懒:alt={cab.name} 该写还是要写,图片没有 alt 是真实的可访问性缺陷, 面试和 code review 都会提。
现在说 key。两个列表,两种情况:
- 车列表(
CabOptions里):key={cab.id}就够了 —— 六辆车的 id 各不相同,而且列表不会变。 - 历史列表(
RideHistory里):key={ride.id}不安全 ——同一辆车可以被订两次, 历史里就有两条id: 1。 React 会警告Encountered two children with the same key, 并且在更新时可能复用错的 DOM 节点。
源项目用的是 key={`${ride.id}-${index}`} ——id 加位置,两条相同的车也能区分开。
会追问:「不是说 index 不能当 key 吗?」——说的是「不要只用 index」。纯 index 的问题是列表中间插入/删除时 key 会错位到别的数据上; 这里 ride.id 提供了身份、index 只用来消歧,而且历史是只在尾部追加的 —— 没有中间插入,index 不会错位。
真正干净的做法是给每条记录一个自己的 id(比如 bookedAt: Date.now()), 这样 key 就有了天然唯一值。 源项目没这么做,但这是面试里可以主动说的加分点。
In one line: five data-testid hooks per card: cab-card-img / -name / -type / -price / -select-button.
The tests only count them, they do not read them — all five assertions are toHaveLength(6). Do not let that make you lazy: still write alt={cab.name}, because an image with no alt is a real accessibility defect and both interviews and code review will call it out.
Now the keys. Two lists, two situations:
- The cab list (in
CabOptions):key={cab.id}is enough — the six ids are distinct and the list never changes. - The history list (in
RideHistory):key={ride.id}is not safe — the same cab can be booked twice, so the history holds two entries withid: 1. React warnsEncountered two children with the same keyand may reuse the wrong DOM node on update.
The source project uses key={`${ride.id}-${index}`} — id plus position, so two identical cabs stay distinct.
Follow-up: “I thought index must never be a key?” — the rule is “never index alone”. Bare index breaks when items are inserted or removed in the middle, because a key then lands on different data. Here ride.id supplies identity and index only disambiguates — and the history is append-only at the tail, so nothing shifts.
The genuinely clean fix is to give each record its own id (say bookedAt: Date.now()), which makes the key naturally unique. The source project does not, but it is a good point to raise yourself in an interview.
cab-booking-context/src/components/CabOptions/CabCard.jsx动手做Get your hands on it
填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.
id: 1)。 历史列表用下面哪个 key 会报「重复 key」警告?The user books the same Ford Fusion (id: 1) twice in a row. Which of these keys makes the history list warn about duplicate keys?alt 和 type="button"。You only get the props signature. Write the five testids yourself, and do not forget alt and type="button".- 五个 data-testid 一个不少:cab-card-img / -name / -type / -price / -select-buttonAll five data-testid values: cab-card-img / -name / -type / -price / -select-button
- img 要有 alt(测试不查,但没有 alt 是真实的可访问性缺陷)The img needs an alt (the tests do not check it, but a missing alt is a real accessibility defect)
- 按钮写 type="button" —— 不写的话在 <form> 里会变成提交按钮The button needs type="button" — without it, inside a <form> it becomes a submit button
- onClick 里包一层箭头函数把 cab 传进去,不是直接传 onSelectCabWrap the onClick in an arrow function that passes cab in; do not pass onSelectCab directly
- 价格前面要有 $ —— 历史列表的断言查的是 "$20"The price needs a $ in front — the history assertion looks for "$20"
看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.
初学者常见的几种写法错误Mistakes beginners actually make
下面每一段都是「能编译、但结果不对」或者「一跑就炸」的真实写法。先自己看出问题在哪,再看解释。Every snippet below either compiles and gives the wrong answer, or blows up on the first run. Spot the problem yourself before reading the explanation.
sort() 默认按字符串比较,大写字母在前, 出来是 Luxury → SUV → Sedan。而
data.json 的键顺序本来就是对的。这个错误的模式很典型:看到列表就想「是不是该排一下」 —— 但数据源已经表达了顺序意图时,任何加工都是破坏。顺带:
sort() 还会原地修改它作用的数组。 这里作用在 Object.keys() 返回的新数组上所以无害, 但直接 someStateArray.sort() 就会改到 state。sort() compares as strings by default, and capital letters come first, so the result is Luxury → SUV → Sedan.The key order in
data.json was already correct. The mistake follows a very common pattern: you see a list and wonder whether it should be sorted. When the data source already states the order, any extra step breaks it.One more point:
sort() also changes the array in place. Here it runs on the new array returned by Object.keys(), so it does no harm, but someStateArray.sort() would change the state itself.onClick={onSelectCab} 等于onSelectCab(clickEvent), 而 cab 从来没被传出去。症状很迷惑:不报错,流程也走得通 —— 页面照样切到 loading、再切到确认页, 只是车名变成了空白(因为
?.name 是 undefined,React 什么也不渲染)。 测试 3 会挂在toHaveTextContent("Ford Fusion is on the way…")。需要传自己的参数,就包一层:
onClick={() => onSelectCab(cab)}。A DOM event handler always receives the event object as its first argument. So onClick={onSelectCab} means onSelectCab(clickEvent), and cab is never passed at all.The symptom is confusing: no error, and the flow still works — the page moves to loading and then to the confirmation page, only the cab name is blank (because
?.name is undefined, and React renders nothing for it). Test 3 fails on toHaveTextContent("Ford Fusion is on the way…").To pass an argument of your own, wrap it:
onClick={() => onSelectCab(cab)}.换一道题也能用Works on other problems too
考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.
- 3 个类型 × 2 辆车 = 6 张卡,五个 toHaveLength(6) 就是这么来的。3 types × 2 cabs = 6 cards, which is where the five toHaveLength(6) checks come from.
- 分组顺序来自 data.json 的键插入顺序,Object.keys 直接给你,别 sort。The group order comes from the order the keys were written in data.json. Object.keys hands it to you, so do not sort.
- CabCard 五个 testid:img / name / type / price / select-button。The five testids of CabCard: img, name, type, price, select-button.
- 历史列表的 key 不能只用 ride.id —— 同一辆车能订两次。The key in the history list cannot be ride.id alone, because the same cab can be booked twice.
- onClick={onSelectCab} 会把事件对象当 cab 传进去,必须包箭头函数。onClick={onSelectCab} passes the event object in place of cab, so you have to wrap it in an arrow function.