DrillLab

Cab Booking(Context 版)Cab Booking (Context version)

React困难 · Hard约 45 分钟~45 min浏览器里能跑Runs in the browser
§01

题面The problem

先把要求读完,再动手。Read every requirement before you start.

一个四页面的打车应用。给定数据、卡片和选车页,要你写的是 Context 那一层加三个页面:Context 存「当前预订」和「行程历史」,一个 action 同时改两个 state,四个页面用一个字符串状态机切换,历史只留最新三条且最新在最上。You get the requirements and the file list only. No code at all. If you get stuck, work through the four levels of hints. The answer sits behind the last door.
验收标准Acceptance criteria
  • Context 存两样:bookedCabDetails(当前预订,初始 null)、rideHistory(全部行程,初始 [])The Context holds two things: bookedCabDetails (the current booking, starts as null) and rideHistory (every ride, starts as [])
  • updateBookedCabDetails(cab) 一次干两件事:设成当前预订 + 追加进历史。追加要造新数组,不许 pushupdateBookedCabDetails(cab) does two things in one call: it sets the current booking and appends to the history. The append has to build a new array, never push
  • useCabContext() 自定义 hook 带守卫:没套 Provider 就抛错,信息里要有 CabProvider。不许给 createContext 假默认值把错误兜住The custom hook useCabContext() carries a guard: with no Provider around it, it throws, and the message contains CabProvider. Do not cover up that error by giving createContext a fake default value
  • 一个字符串 state 管四个页面(home / cab-options / loading / cab-confirmation),初始 home。不要用多个 booleanOne string state drives all four pages (home / cab-options / loading / cab-confirmation) and starts at home. Do not use several booleans
  • 点 book-button → cab-options;此时首页必须消失(一次只显示一个页面)Clicking book-button goes to cab-options, and the home page must disappear. Only one page is on screen at a time
  • 点某张卡的 Select → 先写 Context 再进 loading,两件事在同一个 handler 里Clicking Select on a card writes to the Context first and then moves to loading. Both happen inside the same handler
  • Loading 挂载 1000ms 后调 onComplete;用 setTimeout 不是 setIntervalLoading calls onComplete 1000ms after it mounts. Use setTimeout, not setInterval
  • Loading 的 effect 必须有清理函数 —— 卸载后不许再调 onCompleteThe effect in Loading must have a cleanup function. Once it has unmounted it must not call onComplete
  • 确认页 confirm-message 显示「<车名> is on the way and will arrive shortly.」,bookedCabDetails 初始为 null 所以要用 ?.On the confirmation page, confirm-message reads "<cab name> is on the way and will arrive shortly." bookedCabDetails starts as null, so you need ?.
  • 点 confirm-button 回首页,此时历史里能看到刚订的车Clicking confirm-button returns to the home page, and the ride you just booked is visible in the history
  • 历史只显示最新三条、最新的排最上面;reverse() 原地修改,别翻到 state 上The history shows only the newest three rides, newest at the top. reverse() changes the array in place, so do not run it on the state array
  • 有记录时每条一个 <li data-testid="history-cabs">(含车名和 $价格);没记录时只显示 <p data-testid="no-ride-title">No ride history yet.</p>,两者互斥With rides in the history, each one is a <li data-testid="history-cabs"> holding the cab name and the $ price. With none, the only thing on screen is <p data-testid="no-ride-title">No ride history yet.</p>. The two never appear together

预计 45 分钟。这个数字是照「读完题就开始写、不查资料」估的 —— 第一次超时很正常,第二次要压进去。Budget 45 minutes. Overrunning on the first pass is normal; the second pass should fit.

§02

工作区Workspace

工作区是一个真的浏览器沙箱:左边写代码,右边实时预览,下面一个「跑测试」按钮。测试和本机那套是同一批断言,转写成了浏览器里能跑的写法。The workspace is a real in-browser sandbox: edit on the left, live preview on the right, one Run button below. The assertions are the same ones that pass on a real machine, rewritten for the browser runner.

需要联网。Requires an internet connection. 打包器和 npm 依赖都在 CodeSandbox 的远程服务上(评估过程见 docs/sandpack-evaluation.md),断网这块就起不来 —— 那就照下面的命令在本机跑。The bundler and the npm packages come from CodeSandbox's remote service, so this panel needs network access.

10 个起始文件 · 目标 10 passed10 starter files · target 10 passed
自己写出来、测试全绿之后再打勾。看懂答案不算。Tick this only after you wrote it yourself and the tests went green.
§03

展开讲解Walkthrough

下面是《Context 放在哪一层 —— 这道题最容易死的地方》那一节的原文 —— 和课程里是同一份内容,不是另写的摘要。卡住了再展开。Below is the actual text of the lesson “Context 放在哪一层 —— 这道题最容易死的地方” — the same content as in the course, not a rewritten summary. Expand it when you stall.

展开《Context 放在哪一层 —— 这道题最容易死的地方》(3 段 · 约 16 分钟)Expand “Context 放在哪一层 —— 这道题最容易死的地方” (3 sections · ~16 min)

完整那一节(含练习、常见错误、迁移模式)在The full lesson (with exercises, common mistakes and transfer patterns) is at Context 放在哪一层 —— 这道题最容易死的地方

§01

Context 三件套The three parts of Context

createContext 造管道、Provider 灌数据、自定义 hook 取数据createContext makes the channel, the Provider fills it with data, and a custom hook reads the data

一句话:createContext() 造一根管道,Provider 往管道里灌值, 子树里任何组件用 useContext 就能取到 ——不用一层层传 props

三件套各自的职责:

  • const CabContext = createContext()—— 造管道。注意源项目没给默认值, 所以没套 Provider 时 useContext 返回 undefined。这是故意的,见下一条。
  • CabProvider —— 一个普通组件。 两个 useState 存状态, 一个函数改状态, 三样东西打包成 value 灌进 Provider
  • useCabContext —— 自定义 hook。 它不只是 useContext 的别名,它还带一个守卫:拿不到就抛错。

那个守卫在防什么(下面代码块里高亮的 26–28 行): 如果没有守卫,忘了套 Provider 时 context undefined, 然后 const { rideHistory } = undefined 会抛一个看不懂的解构错误Cannot destructure property ... of undefined)。 有了守卫,报错直接说useCabContext must be used within a CabProvider ——把「忘了套 Provider」这个真正的原因摆在你脸上。

会追问:「为什么不给 createContext 一个默认值?」—— 给了默认值,忘套 Provider 时代码会静默地用假数据跑下去, 你会以为功能坏了而不是配置错了。不给默认值 + 守卫抛错,是让错误尽早暴露。

In one line: createContext() lays a pipe, Provider pours a value into it, and any component in the subtree reads it with useContext no prop drilling.

What each of the three parts is for:

  • const CabContext = createContext() — the pipe. Note the source project gives no default value, so without a Provider useContext returns undefined. That is deliberate; see below.
  • CabProvider — an ordinary component. Two useState calls hold the state, one function changes it, and all three go into value.
  • useCabContext — a custom hook. It is not just an alias for useContext; it carries a guard that throws when there is nothing to read.

What that guard is protecting you from (the highlighted lines 26–28 in the code below): without it, forgetting the Provider leaves context as undefined, and then const { rideHistory } = undefined throws an opaque destructuring error (Cannot destructure property ... of undefined). With the guard you get useCabContext must be used within a CabProvider the actual cause, stated plainly.

Follow-up: “Why not give createContext a default value?” — because then forgetting the Provider makes the code quietly run on fake data, and you go hunting for a broken feature instead of a missing wrapper. No default plus a throwing guard makes the mistake surface immediately.

JSXsrc/context/CabContext.js(源项目原文 —— 注意扩展名)src/context/CabContext.js (as in the source project — note the extension)源项目From source
1import { createContext, useContext, useState } from "react";
2
3const CabContext = createContext();
4
5const CabProvider = ({ children }) => {
6 const [bookedCabDetails, setBookedCabDetails] = useState(null);
7 const [rideHistory, setRideHistory] = useState([]);
8
9 const updateBookedCabDetails = (details) => {
10 setBookedCabDetails(details);
11 setRideHistory([...rideHistory, details]);
12 };
13
14 return (
15 <CabContext.Provider
16 value={{ bookedCabDetails, updateBookedCabDetails, rideHistory }}
17 >
18 {children}
19 </CabContext.Provider>
20 );
21};
22
23const useCabContext = () => {
24 const context = useContext(CabContext);
25
26 if (!context) {
27 throw new Error("useCabContext must be used within a CabProvider");
28 }
29
30 return context;
31};
32
33export { CabProvider, useCabContext };
Source: cab-booking-context/src/context/CabContext.js
§02

Provider 必须在 App 外面The Provider must sit outside App

因为 App 自己就是一个消费者Because App itself is one of the readers

一句话:一个组件读不到自己 return 里提供的 Context。useContext找 Provider, 不往下找。

为什么这道题特别容易死在这儿:看下面 App 的第 14 行 ——App 自己调了 useCabContext(), 因为 handleSelectCab 需要updateBookedCabDetails所以 App 是消费者,不是提供者。

如果你把 Provider 写进 App 的 return 里:App 顶部那句 useCabContext()渲染 App 自己的时候就执行了, 那时候 Provider 还没挂上 —— context undefined, 守卫立刻抛 useCabContext must be used within a CabProvider四个测试全红

正确的层级:index.jsx<CabProvider>包住 <App />测试文件里也是同样的包法 —— 这不是巧合,是它在告诉你 Provider 该在哪一层。

会追问:「那 Provider 能不能再往上,包在 StrictMode 外面?」—— 能,位置只要在所有消费者之上就行。 源项目放在 StrictMode 里面, 好处是开发模式下的双次渲染也会覆盖到 Provider, 能更早暴露副作用写在渲染里这类问题。

In one line: a component cannot read a Context it provides in its own return. useContext looks up the tree for a Provider, never down.

Why this question in particular punishes it: look at line 14 of App below — App calls useCabContext() itself, because handleSelectCab needs updateBookedCabDetails. So App is a consumer, not the provider.

Put the Provider inside App’s return and that useCabContext() at the top of App runs while App itself is rendering, before any Provider is mounted. context is undefined, the guard throws useCabContext must be used within a CabProvider, and all four tests go red.

The right layering: in index.jsx, <CabProvider> wraps <App />. The test file wraps it the same way — that is not a coincidence, it is the test telling you where the Provider belongs.

Follow-up: “Could the Provider go even higher, outside StrictMode?” — yes; it only has to sit above every consumer. The source project keeps it inside StrictMode, which means the development double-render covers the Provider too and surfaces things like effects written into render sooner.

JSXsrc/index.jsx(Provider 在 App 外面)src/index.jsx (the Provider wraps App)源项目From source
1import React from "react";
2import ReactDOM from "react-dom/client";
3import App from "./App";
4import { CabProvider } from "./context/CabContext";
5import "./index.css";
6
7ReactDOM.createRoot(document.getElementById("root")).render(
8 <React.StrictMode>
9 <CabProvider>
10 <App />
11 </CabProvider>
12 </React.StrictMode>,
13);
Source: cab-booking-context/src/index.jsx
JSX把 Provider 放错层级会怎样(示意)What happens when the Provider sits on the wrong level (illustration)示意Illustrative
1// ✕ 反例:Provider 写在 App 内部
2const App = () => {
3 const [currentPage, setCurrentPage] = useState("home");
4 const { updateBookedCabDetails } = useCabContext(); // ← 这一行先执行
5 // 此时下面那个 Provider 还没挂上
6 return (
7 <CabProvider> {/* ← 太晚了 */}
8 <div className="App"></div>
9 </CabProvider>
10 );
11};
12
13// 实际报错:
14// Error: useCabContext must be used within a CabProvider
15// → 四个测试全红,而且报错指向 App,很容易以为是 App 写错了
1// ✕ wrong: the Provider is written inside App
2const App = () => {
3 const [currentPage, setCurrentPage] = useState("home");
4 const { updateBookedCabDetails } = useCabContext(); // ← this line runs first
5 // the Provider below is not mounted yet
6 return (
7 <CabProvider> {/* ← 太晚了 */}
8 <div className="App"></div>
9 </CabProvider>
10 );
11};
12
13// the actual error:
14// Error: useCabContext must be used within a CabProvider
15// → all four tests fail, and the error points at App, so App looks like the culprit
§03

一个 action 同时改两个 stateOne action changes two pieces of state

选一辆车 = 设为当前 + 追加进历史Picking a cab means two things: set it as the current booking, and add it to the history

一句话:updateBookedCabDetails(details) 做两件事 ——把这辆车设成「当前预订」并且把它追加进历史

为什么这两件事必须在一个函数里:如果让调用方自己调两次 (setBookedCabDetails(cab) setRideHistory(...)), 那么「选车」这个业务动作就散在调用方了 —— 以后加一条「同一辆车不重复记录」的规则, 你得去每个调用点改。把动作包成一个函数,规则就只有一个地方。

Context 里最终暴露三样东西:

  • bookedCabDetails —— 当前选中的车,CabConfirmation 用它显示 “X is on the way”
  • rideHistory —— 全部记录,RideHistory 用它取最新三条
  • updateBookedCabDetails —— 唯一的写入口,App 用它

注意消费者是散开的:App 只要写、CabConfirmation 只要bookedCabDetailsRideHistory 只要 rideHistory三个组件各取所需,互相不知道对方存在 —— 这就是用 Context 而不是 props 的收益。 用 props 的话,rideHistory 得从 App 一路传到Home 再传到 RideHistory, 而 Home 自己根本不用它。

In one line: updateBookedCabDetails(details) does two things — marks this cab as the current booking and appends it to the history.

Why both belong in one function: if the caller had to make two calls (setBookedCabDetails(cab) plus setRideHistory(...)), then the business action “pick a cab” would live in the callers. Add a rule later — say, do not log the same cab twice — and you have to edit every call site. One function, one place for the rule.

The Context ends up exposing three things:

  • bookedCabDetails — the current cab; CabConfirmation uses it for “X is on the way”
  • rideHistory — every record; RideHistory takes the newest three from it
  • updateBookedCabDetails — the only way to write; App uses it

Notice how spread out the consumers are: App only writes, CabConfirmation only needs bookedCabDetails, RideHistory only needs rideHistory. Three components take what they need and know nothing about each other — that is the payoff over props. With props, rideHistory would have to travel from App through Home into RideHistory, and Home has no use for it at all.

JSXsrc/App.jsx(唯一的写入口在这里被调用)src/App.jsx (the only write path is called here)源项目From source
1import { useState } from "react";
2import "./App.css";
3import { AppHeader } from "./components/AppHeader";
4import Home from "./components/Home/Home";
5import CabOptions from "./components/CabOptions/CabOptions";
6import Loading from "./components/Loading/Loading";
7import CabConfirmation from "./components/CabConfirmation/CabConfirmation";
8import { useCabContext } from "./context/CabContext";
9
10const title = "Cab Booking";
11
12const App = () => {
13 const [currentPage, setCurrentPage] = useState("home");
14 const { updateBookedCabDetails } = useCabContext();
15
16 const handleSelectCab = (cab) => {
17 updateBookedCabDetails(cab);
18 setCurrentPage("loading");
19 };
20
21 return (
22 <div className="App">
23 <AppHeader title={title} />
24
25 {currentPage === "home" && (
26 <Home onBookClick={() => setCurrentPage("cab-options")} />
27 )}
28
29 {currentPage === "cab-options" && (
30 <CabOptions onSelectCab={handleSelectCab} />
31 )}
32
33 {currentPage === "loading" && (
34 <Loading onComplete={() => setCurrentPage("cab-confirmation")} />
35 )}
36
37 {currentPage === "cab-confirmation" && (
38 <CabConfirmation onConfirm={() => setCurrentPage("home")} />
39 )}
40 </div>
41 );
42};
43
44export default App;
Source: cab-booking-context/src/App.jsx
§04

参考答案Reference solution

这道题没有配套的分级提示 —— 卡住了先看上面的讲解那一节。This problem has no graded hints. If you are stuck, read the walkthrough above first.

这份答案在本机真跑过测试。但先确认你自己动手写过一遍 —— 读懂答案和写出答案是两种能力,考场上考的是后一种。This answer really was run here and its tests passed. But write it yourself first — reading an answer and producing one are two different skills, and the exam tests the second.