用一个 state 管四个页面Controlling four pages with one piece of state
没有 react-router。currentPage 是个字符串状态机,四个 && 决定谁显示。There is no react-router. currentPage is a string state machine, and four && checks decide which page shows.
这一页有什么On this page5
- 用一个 currentPage state 管四个页面Control four pages with a single currentPage state
- 说清 && 条件渲染和三元的区别,以及为什么这里用 &&Explain the difference between && and a ternary in conditional rendering, and why && fits here
- 知道为什么 handleSelectCab 必须写在 App 里,而不是 CabCard 里Know why handleSelectCab has to live in App and not in CabCard
- 看懂四个页面之间的转移图Read the transition diagram between the four pages
题目没给路由,所以你得自己决定「页面」怎么表示。写成四个 boolean(isHome / isLoading …)能跑,但两个同时为 true 时会同时渲染两个页面,测试 3 的 getByTestId 会因为找到多个而抛错。一个字符串 state 从根上排除了这种状态。The task gives you no router, so you have to decide how a page is represented. Four booleans (isHome, isLoading and so on) can work, but when two of them are true at the same time two pages render together, and the getByTestId in test 3 throws because it finds more than one match. A single string state rules that situation out from the start.
cab-booking-context/src/App.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/App.jsxcab-booking-context/src/components/Home/Home.jsx首页,把 onBookClick 往上抛The home page; it raises onBookClick upwards
cab-booking-context/src/components/Home/Home.jsx四个页面 = 一个字符串 stateFour pages, one string state
转移图画出来,代码就是照抄Draw the transition diagram and the code just copies it
一句话:currentPage 只可能是四个字符串之一, 每个页面用 && 判断自己该不该出现。
先画转移图:
| 当前 | 触发 | 去哪 |
|---|---|---|
home | 点 book-button | cab-options |
cab-options | 点某张卡的 Select | loading(同时写入 Context) |
loading | 1 秒后自动 | cab-confirmation |
cab-confirmation | 点 confirm-button | home |
四条转移,四个回调。代码里就是四个setCurrentPage(...), 分别通过 onBookClick / onSelectCab /onComplete / onConfirm 传下去。子组件一个都不知道「页面」这回事 —— 它们只知道自己有个回调要调。
为什么用 && 而不是三元:这里是「四选一」,用嵌套三元会变成a ? X : b ? Y : c ? Z : W, 读起来累而且加第五个页面要改结构。 四个平行的 && 一行一个页面,加页面就加一行。
会追问:「为什么不用四个 boolean?」—— 因为 boolean 允许非法状态。isLoading 和 isHome 同时为 true 时页面上会有两个 <main>,getByTestId 找到多个直接抛错。用一个字符串,非法状态在类型层面就不存在 —— 这个思路叫「让不可能的状态无法表示」。
In one line: currentPage can only be one of four strings, and each page uses && to decide whether it should show.
Draw the transition table first:
| From | Trigger | To |
|---|---|---|
home | click book-button | cab-options |
cab-options | click a card’s Select | loading (and write to Context) |
loading | automatic after 1s | cab-confirmation |
cab-confirmation | click confirm-button | home |
Four transitions, four callbacks. In code that is four setCurrentPage(...) calls, handed down as onBookClick / onSelectCab / onComplete / onConfirm. No child component knows that “pages” exist — each one only knows it has a callback to fire.
Why && and not a ternary: this is one-of-four. Nested ternaries turn into a ? X : b ? Y : c ? Z : W, which is tiring to read and has to be restructured to add a fifth page. Four parallel && lines give you one line per page, so adding a page means adding a line.
Follow-up: “Why not four booleans?” — because booleans permit illegal states. With isLoading and isHome both true the page has two <main> elements, and getByTestId throws when it matches more than one. One string means the illegal state cannot be represented at all — the “make impossible states unrepresentable” idea.
cab-booking-context/src/App.jsx为什么 handleSelectCab 在 App 里Why handleSelectCab lives in App
因为它要同时干两件事,而其中一件只有 App 知道Because it has to do two things at once, and only App can do one of them
一句话:选一辆车要写 Context 加 切页面, 而「切页面」这件事只有 App 做得到 ——currentPage 在它手里。
换个问法:CabCard 能不能自己调updateBookedCabDetails?技术上能 —— 它也在 Provider 底下,useCabContext() 一样能用。 但它没法切页面, 所以还是得往上抛一个回调。既然回调躲不掉,就把两件事都放在回调里, 别拆成「Card 写数据 + App 切页面」两半。
拆成两半会出什么问题:「选车」这个动作变成 两个组件配合完成的,顺序和完整性没人保证 —— 以后有人在 CabCard 里加个提前 return, 就会出现「页面切了但 Context 没写」, 确认页显示 undefined is on the way。
看 CabCard 有多干净:它只有 onClick={() => onSelectCab(cab)},完全不知道 Context 存在,也不知道有页面这回事。 这种组件最好测、最好复用。
会追问:「这不就是状态提升(lifting state up)吗?」—— 对。规则是:状态放在所有需要它的组件的最近公共祖先。currentPage 四个页面都要用, 公共祖先就是 App;rideHistory 的消费者跨了三层, 提升到 App 还得往下传, 所以它去了 Context。两种手段解决的是同一个问题,只是距离不同。
In one line: picking a cab has to write to Context and switch the page, and only App can do the second one — currentPage lives there.
Put it another way: could CabCard call updateBookedCabDetails itself? Technically yes — it is under the Provider too, so useCabContext() works. But it cannot switch the page, so it still needs to hand a callback upward. Since the callback is unavoidable, put both jobs inside it rather than splitting into “Card writes data, App switches page”.
What splitting costs you: “pick a cab” becomes an action two components perform together, and nothing guarantees order or completeness. Somebody adds an early return in CabCard later and you get “page switched but Context never written”, so the confirmation page reads undefined is on the way.
Notice how clean CabCard is: it only has onClick={() => onSelectCab(cab)} and knows nothing about Context and nothing about pages. That is the easiest kind of component to test and to reuse.
Follow-up: “Isn’t this just lifting state up?” — yes. The rule is: state belongs in the closest common ancestor of every component that needs it. All four pages need currentPage, and their common ancestor is App. The consumers of rideHistory sit three levels apart, so lifting it to App would still mean drilling it down — which is why it went into Context instead. Both tools solve the same problem; they differ in distance.
cab-booking-context/src/components/Home/Home.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.
初学者常见的几种写法错误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.
一个字符串 state 只有 4 种取值,
setCurrentPage("loading")天然就把别的页面关掉了。 这不是「写法更漂亮」,是把一整类 bug 从可能变成不可能。Four booleans have 16 combinations, and only 4 of them are valid. On every page change you have to remember to turn one on and one off, and if you forget one, two pages are on the screen at the same time.A single string state has only 4 possible values, so
setCurrentPage("loading") turns the other pages off by itself. This is not about code that reads better. It moves a whole class of bug from possible to impossible.换一道题也能用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.
- 四个页面用一个 currentPage 字符串管,四个 && 各判一次。One currentPage string controls the four pages, with one && check for each.
- 先画转移表:四条转移就是四个回调,代码照抄。Draw the transition table first: the four transitions become four callbacks, and the code follows the table.
- handleSelectCab 放在 App 里,因为「切页面」只有 App 做得到。handleSelectCab goes in App, because only App can change the page.
- onSelectCab={handleSelectCab} 不能加括号 —— 加了会在渲染时执行并无限重渲染。onSelectCab={handleSelectCab} must have no parentheses. With them the function runs during render and the component re-renders without end.
- RideHistory 挂在首页里,所以点完确认回首页就能看到新记录。RideHistory sits on the home page, so when you confirm and come back home the new entry is there.