DrillLab
第 02 / 08 节LESSON 02 / 08约 16 分钟~16 min

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.

2 个练习2 exercisesCab Booking · 第 1 部分Cab Booking · Part 1
这一页有什么On this page6
学完这节你会After this lesson you can
  • 写出 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
这在考试里考什么What the exam does with this

这是这道题最容易一次死透的地方。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.

这节课要看的真实文件Real files this lesson looks at高亮行 = 需要你动手改的文件Highlighted rows are the files you edit
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.

JavaScriptCabContext.js源项目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
cab-booking-context/src/index.jsxProvider 包在 App 外面的那一层The layer where the Provider wraps App
JSXindex.jsx源项目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
§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
练习Practice

动手做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.

L2填空Fill the blanks补齐 Context 三件套Fill in the three parts of the Context
四个空。第 4 个空是这道题的守卫,写错了就等于没有守卫。Four blanks. The fourth one is the guard, and getting it wrong is the same as having no guard at all.
JSXsrc/context/CabContext.jsx4 个空4 blanks
1import { createContext, useContext, useState } from "react";
2
3const CabContext = ();
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();
12 };
13
14 return (
15 <CabContext.
16 value={{ bookedCabDetails, updateBookedCabDetails, rideHistory }}
17 >
18 {children}
19 </CabContext.>
20 );
21};
22
23const useCabContext = () => {
24 const context = useContext(CabContext);
25
26 if () {
27 throw new Error("useCabContext must be used within a CabProvider");
28 }
29
30 return context;
31};
32
33export { CabProvider, useCabContext };
把 4 个空都填上才能检查(还差 4 个)Fill all 4 blanks to check (4 to go)
L3写整块Write a block从签名写出整个 CabContextWrite the whole CabContext from the signature
只给你 import 和导出。三件套自己写出来, 包括那个守卫。检查器会查守卫、查不可变更新、 以及不许用 pushYou only get the import and the export. Write the three parts yourself, including the guard. The checker looks for the guard, for an immutable update, and for no use of push.
要求Requirements
  • 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
JSXsrc/context/CabContext.jsx
This check is textual: it looks for the right constructs, it does not run your code
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。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.

错例Wrong

初学者常见的几种写法错误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.

JSX原地修改Changed in place示意Illustrative
1// ✕ 用 push 追加 —— React 看不到变化
2const updateBookedCabDetails = (details) => {
3 setBookedCabDetails(details);
4 rideHistory.push(details); // 原地改了同一个数组
5 setRideHistory(rideHistory); // 传的还是同一个引用
6};
1// ✕ appending with push — React sees no change
2const updateBookedCabDetails = (details) => {
3 setBookedCabDetails(details);
4 rideHistory.push(details); // the same array was changed in place
5 setRideHistory(rideHistory); // the same reference goes back in
6};
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].
JSX把守卫改成兜底The guard turned into a fallback示意Illustrative
1// ✕ 守卫写成了默认值兜底 —— 错误被藏起来了
2const useCabContext = () => {
3 const context = useContext(CabContext);
4 return context ?? { rideHistory: [], bookedCabDetails: null };
5};
1// ✕ the guard became a default fallback — the error is now hidden
2const useCabContext = () => {
3 const context = useContext(CabContext);
4 return context ?? { rideHistory: [], bookedCabDetails: null };
5};
这样忘套 Provider 时不会报错,页面会静默地显示「没有历史记录」。 你会去查 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.
迁移Transfer

换一道题也能用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.

组件读不到自己提供的 ContextA component cannot read the Context it provides itself
useContext 往上找 —— Provider 必须在消费者之上useContext looks upwards: the Provider has to sit above the reader
测试文件自己包了一层 ProviderThe test file wraps a Provider of its own
那是在告诉你 Provider 该在哪一层That tells you which level the Provider belongs on
「Cannot destructure property of undefined」"Cannot destructure property of undefined"
十有八九是忘了套 ProviderAlmost always a missing Provider
一个业务动作要改两个 stateOne user action has to change two pieces of state
包成 Context 里的一个函数,别让调用方调两次Wrap it in one function inside the Context; do not make the caller call twice
这节的要点What to take away
  1. 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.
  2. Provider 必须在 App 外面 —— App 自己就是消费者(handleSelectCab 要写入)。The Provider must sit outside App, because App is a reader itself: handleSelectCab writes to the Context.
  3. createContext 不给默认值 + 守卫抛错,是为了让「忘套 Provider」立刻暴露。Calling createContext with no default value, plus a guard that throws, makes a forgotten Provider show up immediately.
  4. updateBookedCabDetails 一次改两个 state,业务规则集中在一处。updateBookedCabDetails changes two pieces of state in one call, which keeps the rule in one place.
  5. 追加历史必须造新数组,push 会让 React 跳过重渲染。Adding to the history has to build a new array; push makes React skip the re-render.

接下来What next

  1. 把这一节的练习做掉Do this lesson’s exercises2 个,就在这一页上面 —— 别攒着最后一起做2 of them, further up this page — do not save them for later
    回到练习 ↑Back up to them ↑
  2. 接着看下一节Continue to the next lesson用一个 state 管四个页面Controlling four pages with one piece of state
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: 先读四个测试:它们到底要什么Read the four tests first: what exactly they ask for