Context 放在哪一层 —— 这道题最容易死的地方Which level the Context goes on — the most common way to fail this task
Provider 必须包在 App 外面。包在里面,App 自己就用不了 Context。The Provider has to wrap App from the outside. Put it inside App and App itself cannot read the Context.
这一页有什么On this page6
- 写出 Context 三件套:createContext / Provider / 自定义 hookWrite the three parts of Context: createContext, the Provider, and a custom hook
- 说清为什么 Provider 必须在 App 外面,而不是 App 内部Explain why the Provider must sit outside App and not inside it
- 知道自定义 hook 里那个 throw 守卫在防什么Know what the throw guard in the custom hook protects you from
- 看懂测试为什么也要自己包一层 CabProviderSee why the test file also wraps its own CabProvider
这是这道题最容易一次死透的地方。App 里的 handleSelectCab 要调 updateBookedCabDetails,所以 App 本身就是一个消费者 —— 如果你把 Provider 写在 App 的 return 里,App 自己拿不到 context,那个 throw 守卫会立刻炸,四个测试全红。This is the fastest way to fail the whole task. handleSelectCab lives in App and calls updateBookedCabDetails, so App itself is a reader of the Context. If you write the Provider inside the return of App, App cannot reach the context, the throw guard fires at once, and all four tests fail.
cab-booking-context/src/context/CabContext.jsContext 三件套。注意扩展名是 .js 而里面有 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/context/CabContext.jscab-booking-context/src/index.jsxProvider 包在 App 外面的那一层The layer where the Provider wraps App
cab-booking-context/src/index.jsxContext 三件套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 ProvideruseContextreturnsundefined. That is deliberate; see below.CabProvider— an ordinary component. TwouseStatecalls hold the state, one function changes it, and all three go intovalue.useCabContext— a custom hook. It is not just an alias foruseContext; 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.
cab-booking-context/src/context/CabContext.jsProvider 必须在 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.
cab-booking-context/src/index.jsx一个 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 只要bookedCabDetails、RideHistory 只要 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;CabConfirmationuses it for “X is on the way”rideHistory— every record;RideHistorytakes the newest three from itupdateBookedCabDetails— the only way to write;Appuses 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.
cab-booking-context/src/App.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.
- createContext() 不传默认值 —— 这样没套 Provider 时是 undefined,守卫才抓得住createContext() takes no default value — that way it is undefined with no Provider around, which is what the guard catches
- 两个 state:bookedCabDetails 初始 null、rideHistory 初始 []Two states: bookedCabDetails starts null, rideHistory starts []
- updateBookedCabDetails 同时改两个 state,历史用不可变更新(展开运算符),不许 pushupdateBookedCabDetails changes both states, and the history update is immutable (spread), never push
- useCabContext 里有 if 守卫 + throw,错误信息要出现 CabProvideruseCabContext has an if guard plus a throw, and the message mentions CabProvider
- 命名导出 CabProvider 和 useCabContextNamed exports for CabProvider and useCabContext
看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。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.
push 改的是同一个数组对象,setRideHistory(rideHistory) 传进去的引用没变 —— React 用 Object.is 比较新旧 state,发现一样就跳过重渲染。症状很迷惑:数据其实变了,但界面不更新; 然后某次别的 state 变化触发重渲染,历史突然一次冒出好几条。必须造新数组:
[...rideHistory, details]。push changes the same array object, so the reference given to setRideHistory(rideHistory) has not changed. React compares old and new state with Object.is and skips the re-render when they are equal.The symptom is confusing: the data really did change, but the screen does not update. Then some other state change causes a re-render and several history rows appear at once. You have to build a new array:
[...rideHistory, details].RideHistory 为什么不显示数据, 而真正的原因在 index.jsx。守卫的意义就是让配置错误在第一时间炸出来, 而不是伪装成功能 bug。这一条面试常问,本站「主题切换(Context + value 记忆化)」 那道题也考同一个点。With this version, forgetting the Provider produces no error. The page quietly shows “no ride history”. You will go and look at why
RideHistory shows no data, while the real cause is in index.jsx.The whole point of the guard is to make a setup mistake fail at once, instead of looking like a bug in a feature. Interviewers ask about this often, and the Theme switch (Context plus a memoised value) task on this site tests the same point.
换一道题也能用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.
- Context 三件套:createContext 造管道、Provider 灌值、自定义 hook 取值 + 守卫。The three parts of Context: createContext makes the channel, the Provider supplies the value, and the custom hook reads it and guards against a missing Provider.
- Provider 必须在 App 外面 —— App 自己就是消费者(handleSelectCab 要写入)。The Provider must sit outside App, because App is a reader itself: handleSelectCab writes to the Context.
- createContext 不给默认值 + 守卫抛错,是为了让「忘套 Provider」立刻暴露。Calling createContext with no default value, plus a guard that throws, makes a forgotten Provider show up immediately.
- updateBookedCabDetails 一次改两个 state,业务规则集中在一处。updateBookedCabDetails changes two pieces of state in one call, which keeps the rule in one place.
- 追加历史必须造新数组,push 会让 React 跳过重渲染。Adding to the history has to build a new array; push makes React skip the re-render.