DrillLab
练习Practice

动手做Get your hands on it

练习跟着课文走 —— 每节课尾都有本课的练习。这一页是全部练习的总库,想集中刷题的时候来。 每个练习都写清了它来自哪一节,卡住了就回去看那一节。Practice follows the lessons — every lesson ends with the exercises for that lesson. This page is the whole library, for when you want to drill in one sitting. Each exercise names the lesson it came from, so you can go back when you stall.

0 / 148个做对过you got right

练习Exercises

筛出 47 个练习(共 148 个) · 第 3 / 4 页。Showing 47 of 148 · page 3 / 4.
来自From 变式五 · 主题切换:Context 怎么用Variation 5 · theme switching: how to use Context · React 考试React exam
L2Debug LabDebug LabDebug Lab · Cannot destructure property 'theme'DrillLab 自出Written by DrillLab

按钮好好的,卡片一渲染就整页白屏。这是真实报错。

The button is fine, and the moment the card renders the whole page goes blank. This is the real error.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
$ npx vitest run src/Theme.test.tsx TypeError: Cannot destructure property 'theme' of '(0 , __vite_ssr_import_1__.useTheme)(...)' as it is undefined. at ThemedCard (src/components/ThemedCard/index.tsx:6:11) at ThemeApp ✕ 默认是 light,按钮说 Switch to Dark ✕ 点一下变 dark:按钮文字和卡片底色一起变 ✕ 再点一下切回 light ✕ 没套 Provider 就用 useTheme(),必须立刻报错 ✕ toggleTheme 是稳定引用:theme 变了它也不变 Tests 5 failed | 3 passed (8) # 浏览器里的报错略有不同,意思一样: # Cannot destructure property 'theme' of 'useTheme(...)' as it is undefined.$ npx vitest run src/Theme.test.tsx TypeError: Cannot destructure property 'theme' of '(0 , __vite_ssr_import_1__.useTheme)(...)' as it is undefined. at ThemedCard (src/components/ThemedCard/index.tsx:6:11) at ThemeApp ✕ 默认是 light,按钮说 Switch to Dark ✕ 点一下变 dark:按钮文字和卡片底色一起变 ✕ 再点一下切回 light ✕ 没套 Provider 就用 useTheme(),必须立刻报错 ✕ toggleTheme 是稳定引用:theme 变了它也不变 Tests 5 failed | 3 passed (8) # The browser wording is slightly different but means the same thing: # Cannot destructure property 'theme' of 'useTheme(...)' as it is undefined.
TSXsrc/components/ThemeApp/index.tsx示意Illustrative
1// ThemeContext.tsx
2const ThemeContext = createContext<ThemeContextValue>(undefined as never);
3
4export function useTheme() {
5 return useContext(ThemeContext); // 没有守卫
6}
7
8// ThemeApp/index.tsx
9const ThemeApp: React.FC = () => (
10 <>
11 <ThemeProvider>
12 <ThemeToggleButton />
13 </ThemeProvider>
14 <ThemedCard />
15 </>
16);
1// ThemeContext.tsx
2const ThemeContext = createContext<ThemeContextValue>(undefined as never);
3
4export function useTheme() {
5 return useContext(ThemeContext); // no guard
6}
7
8// ThemeApp/index.tsx
9const ThemeApp: React.FC = () => (
10 <>
11 <ThemeProvider>
12 <ThemeToggleButton />
13 </ThemeProvider>
14 <ThemedCard />
15 </>
16);
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From Debug Lab · React 十种典型故障Debug Lab · ten typical React failures · React 考试React exam
L2Debug LabDebug Lab故障 1 · 路径大小写Fault 1 · upper and lower case in a path

新建了组件之后启动开发服务器,Vite 直接报错,页面白屏。

You add a new component, start the dev server, and Vite reports an error right away. The page is blank.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
[plugin:vite:import-analysis] Failed to resolve import "./components/notemanager" from "src/App.tsx". Does the file exist? /Users/me/react-notes-app/src/App.tsx:1:24 1 | import NoteManager from "./components/notemanager"; | ^
TSXsrc/App.tsx示意Illustrative
1import NoteManager from "./components/notemanager";
2
3function App() {
4 return <NoteManager />;
5}
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From Debug Lab · React 十种典型故障Debug Lab · ten typical React failures · React 考试React exam
L2Debug LabDebug Lab故障 2 · props 名字对不上Fault 2 · the prop names do not match

重构时把父组件传的 prop 名改了,子组件忘了跟着改。 页面能显示,但点 Delete 直接崩。

During a refactor the prop name passed by the parent was changed, and the child component was not changed to match. The page still renders, but clicking Delete crashes it.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
Uncaught TypeError: onDelete is not a function at onClick (NoteItem/index.tsx:18:29) at HTMLUnknownElement.callCallback # 另外 TypeScript 那边也在报: src/components/NoteTable/index.tsx(20,7): error TS2322: Type '{ key: number; note: Note; onRemove: (id: number) => void; onEdit: ... }' is not assignable to type 'IntrinsicAttributes & NoteItemProps'. Property 'onDelete' is missing in type ... but required in type 'NoteItemProps'.Uncaught TypeError: onDelete is not a function at onClick (NoteItem/index.tsx:18:29) at HTMLUnknownElement.callCallback # TypeScript is reporting something too: src/components/NoteTable/index.tsx(20,7): error TS2322: Type '{ key: number; note: Note; onRemove: (id: number) => void; onEdit: ... }' is not assignable to type 'IntrinsicAttributes & NoteItemProps'. Property 'onDelete' is missing in type ... but required in type 'NoteItemProps'.
TSX两处不一致The two places that disagree示意Illustrative
1// NoteTable 里传下去的名字:
2<NoteItem
3 key={note.id}
4 note={note}
5 onRemove={onDelete} // ← 传的是 onRemove
6 onEdit={onEdit}
7/>
8
9// NoteItem 的 props 接口和解构:
10export interface NoteItemProps {
11 note: Note;
12 onDelete: (id: number) => void; // ← 期望的是 onDelete
13 onEdit: (note: Note) => void;
14}
15const NoteItem: React.FC<NoteItemProps> = ({ note, onDelete, onEdit }) => {
1// The name NoteTable passes down:
2<NoteItem
3 key={note.id}
4 note={note}
5 onRemove={onDelete} // ← it passes onRemove
6 onEdit={onEdit}
7/>
8
9// The props interface of NoteItem, and how it destructures them:
10export interface NoteItemProps {
11 note: Note;
12 onDelete: (id: number) => void; // ← it expects onDelete
13 onEdit: (note: Note) => void;
14}
15const NoteItem: React.FC<NoteItemProps> = ({ note, onDelete, onEdit }) => {
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From Debug Lab · React 十种典型故障Debug Lab · ten typical React failures · React 考试React exam
L2Debug LabDebug Lab故障 3 · 测试找不到元素Fault 3 · the test cannot find the element

代码看起来完全正确,手动点也没问题,但两个测试挂了。

The code looks entirely correct and clicking through it by hand works, but two tests fail.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
FAIL src/NoteManager.test.tsx > adds a note TestingLibraryElementError: Unable to find an element by: [data-testid="form-input"] Ignored nodes: comments, script, style <body> <div> <div class="layout-column ..." data-testid="note-manager"> <div class="card ..."> <form data-testid="note-form"> <section class="layout-row ..."> <label class="form-title-label">Title:</label> <input type="text" placeholder="Title" data-testid="title-input" ... /> ...
TSXsrc/components/NoteForm/index.tsx示意Illustrative
1<input
2 type="text"
3 placeholder="Title"
4 value={title}
5 onChange={(e) => setTitle(e.target.value)}
6 data-testid="title-input"
7 className="form-input"
8/>
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From GraphQL 是什么:一份 schema 加一堆 resolverWhat GraphQL is: one schema plus a set of resolvers · Federation 考试Federation exam
L2填空Fill the blanks补全 schema 的关键声明Fill in the key declarations of the schema

照真实 schema.graphql 补全。 三个空分别关系到「入口类型」「枚举」「输入类型」。

Fill this in from the real schema.graphql. The three blanks are the entry type, the enum and the input type.

GRAPHQLsrc/schema.graphql3 个空3 blanks
1 OrderStatus {
2 PENDING
3 PROCESSING
4 SHIPPED
5 DELIVERED
6 CANCELLED
7}
8
9type {
10 order(id: ID!): Order
11 orders(userId: ID!): [Order!]!
12}
13
14 OrderItemInput {
15 productId: ID!
16 quantity: Int!
17}
把 3 个空都填上才能检查(还差 3 个)Fill all 3 blanks to check (3 to go)
来自From 非空、列表,和那个没有 price 的 inputNon-null, lists, and the input that has no price · Federation 考试Federation exam
L2填空Fill the blanks给四个 TODO 各自选对兜底策略Pick the right fallback for each of the four TODOs

照 schema 的非空标记,给每个 resolver 填上正确的返回表达式。 想清楚「这个字段能不能是 null」。

Go by the non-null markers in the schema and write the right return expression for each resolver. Decide first whether the field is allowed to be null.

JSsrc/resolvers/orderResolvers.js3 个空3 blanks
1// schema: orders: [Order!]!
2async orders(user, _, { dataSources }) {
3 const orders = await dataSources.orderDataSource.getOrdersByUserId(user.id);
4 return orders [];
5}
6
7// schema: shippingInfo: ShippingInfo (可空)
8async shippingInfo(parent, _, { loaders }) {
9 const info = await loaders.shippingInfoLoader.load(parent.id);
10 return info ?? ;
11}
12
13// schema: order(id: ID!): Order (可空,但题目要求找不到时抛结构化错误)
14async order(_, { id }, { loaders, correlationId }) {
15 const order = await loaders.orderLoader.load(id);
16 if () {
17 throw new GraphQLError(`Order not found: ${id}`, {
18 extensions: { code: ErrorCodes.ORDER_NOT_FOUND, correlationId }
19 });
20 }
21 return order;
22}
把 3 个空都填上才能检查(还差 3 个)Fill all 3 blanks to check (3 to go)
来自From entity、@key 与 __resolveReferenceentity, @key and __resolveReference · Federation 考试Federation exam
L2填空Fill the blanks补全 entity 声明与引用解析Fill in the entity declaration and the reference resolver

三个空。第一个是 directive,第二个是标记「这不是我的字段」, 第三个是引用解析要返回什么。

Three blanks. The first is a directive, the second marks a field as not belonging to this service, and the third is what the reference resolver returns.

GRAPHQLschema.graphql + orderResolvers.js3 个空3 blanks
1# schema.graphql
2type User (fields: "id") {
3 id: ID!
4 orders: [Order!]!
5}
6
7# orderResolvers.js
8# User: {
9# __resolveReference(user, { dataSources, loaders }) {
10# return { id: };
11# },
12# ...
13# }
把 3 个空都填上才能检查(还差 3 个)Fill all 3 blanks to check (3 to go)
来自From N+1 问题与 DataLoaderThe N+1 problem and DataLoader · Federation 考试Federation exam
L2填空Fill the blanks修好 createOrderLoader 并写出 shippingInfoFix createOrderLoader and write shippingInfo

两个空。第一个要你填对数据源上真实存在的方法名, 第二个要你用 loader 而不是数据源。

Two blanks. The first wants the name of a method that really exists on the data source; the second wants you to go through the loader rather than the data source.

JSsrc/resolvers/orderResolvers.js2 个空2 blanks
1function createOrderLoader(orderDataSource) {
2 return new DataLoader(async orderIds => {
3 const orders = await Promise.all(
4 orderIds.map(id => orderDataSource.(id))
5 );
6 return orders;
7 });
8}
9
10// Order.shippingInfo —— 必须走 loader,否则 N+1 考点没答到
11async shippingInfo(parent, _, { dataSources, loaders, correlationId }) {
12 const shippingInfo = await loaders..load(parent.id);
13 return shippingInfo ?? null;
14}
把 2 个空都填上才能检查(还差 2 个)Fill all 2 blanks to check (2 to go)
来自From N+1 问题与 DataLoaderThe N+1 problem and DataLoader · Federation 考试Federation exam
L2Debug LabDebug LabDebug Lab · DataLoader 报 is not a functionDebug Lab · DataLoader reports is not a function

npm test,其中一个 DataLoader 相关的测试挂了。 报错指向 loader 内部。

You run npm test and one of the DataLoader tests fails. The error points inside the loader.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
● Order Resolvers › DataLoader functionality › should batch multiple order requests TypeError: orderDataSource.getOrderById is not a function 29 | 30 | const orders = await Promise.all( > 31 | orderIds.map(id => orderDataSource.getOrderById(id)) | ^ 32 | ); 33 | 34 | return orders; at src/resolvers/orderResolvers.js:31:42 at Array.map (<anonymous>) at DataLoader._batchLoadFn (src/resolvers/orderResolvers.js:31:16)
JavaScriptsrc/resolvers/orderResolvers.js示意Illustrative
1function createOrderLoader(orderDataSource) {
2 return new DataLoader(async orderIds => {
3 const orders = await Promise.all(
4 orderIds.map(id => orderDataSource.getOrderById(id))
5 );
6 return orders;
7 });
8}
9
10// 参考:OrderDataSource 上真实存在的方法
11// class OrderDataSource {
12// async getOrder(id) { ... }
13// async getOrdersByUserId(userId) { ... }
14// async createOrder(userId, items) { ... }
15// }
1function createOrderLoader(orderDataSource) {
2 return new DataLoader(async orderIds => {
3 const orders = await Promise.all(
4 orderIds.map(id => orderDataSource.getOrderById(id))
5 );
6 return orders;
7 });
8}
9
10// For reference: the methods OrderDataSource really has
11// class OrderDataSource {
12// async getOrder(id) { ... }
13// async getOrdersByUserId(userId) { ... }
14// async createOrder(userId, items) { ... }
15// }
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From TODO 1 · User.ordersTODO 1 · User.orders · Federation 考试Federation exam
L2填空Fill the blanks补全 User.ordersFill in User.orders

四个空。第 2 个是数据源上的真实方法名, 第 4 个是那行最容易漏的防御。

Four blanks. The second is the method name that really exists on the data source; the fourth is the guard people most often forget.

JSsrc/resolvers/orderResolvers.js4 个空4 blanks
1async orders(user, _, { dataSources, loaders, correlationId }) {
2 try {
3 console.log(`[${correlationId}] Resolving User.orders for userId: ${user.}`);
4
5 const orders = await dataSources.orderDataSource.(user.id);
6
7 return orders [];
8 } catch (error) {
9 if (error GraphQLError) throw error;
10
11 console.error(`[${correlationId}] Error:`, error.message);
12 throw new GraphQLError('Failed to fetch orders for user', {
13 extensions: { code: ErrorCodes.SERVICE_ERROR, correlationId }
14 });
15 }
16}
把 4 个空都填上才能检查(还差 4 个)Fill all 4 blanks to check (4 to go)
来自From TODO 2 · Order.shippingInfoTODO 2 · Order.shippingInfo · Federation 考试Federation exam
L2填空Fill the blanks补全 Order.shippingInfoFill in Order.shippingInfo

三个空。第 1 个决定你答不答得到 N+1 考点, 第 3 个决定第二条测试过不过。

Three blanks. The first decides whether you answer the N+1 question at all; the third decides whether the second test passes.

JSsrc/resolvers/orderResolvers.js3 个空3 blanks
1async shippingInfo(parent, _, { dataSources, loaders, correlationId }) {
2 try {
3 const shippingInfo = await .shippingInfoLoader.load(parent.);
4
5 return shippingInfo ?? ;
6 } catch (error) {
7 if (error instanceof GraphQLError) throw error;
8 console.error(`[${correlationId}] Error:`, error.message);
9 throw new GraphQLError('Failed to fetch shipping info', {
10 extensions: { code: ErrorCodes.SERVICE_ERROR, correlationId, orderId: parent.id }
11 });
12 }
13}
把 3 个空都填上才能检查(还差 3 个)Fill all 3 blanks to check (3 to go)
来自From TODO 3 & 4 · Query.order 与 Query.ordersTODO 3 & 4 · Query.order and Query.orders · Federation 考试Federation exam
L2填空Fill the blanks补全两个 Query resolverFill in both Query resolvers

四个空横跨两个 resolver。注意它们数据来源不同、兜底策略不同。

Four blanks across two resolvers. Note they read from different places and need different fallbacks.

JSsrc/resolvers/orderResolvers.js4 个空4 blanks
1// schema: order(id: ID!): Order (可空)
2async order(_, { id }, { dataSources, loaders, correlationId }) {
3 try {
4 const order = await loaders..load(id);
5
6 if (!order) {
7 throw new GraphQLError(`Order not found: ${id}`, {
8 extensions: { code: ErrorCodes., correlationId }
9 });
10 }
11 return order;
12 } catch (error) {
13 if (error instanceof GraphQLError) throw error;
14 throw new GraphQLError('Failed to fetch order', {
15 extensions: { code: ErrorCodes.SERVICE_ERROR, correlationId }
16 });
17 }
18}
19
20// schema: orders(userId: ID!): [Order!]! (双重非空)
21async orders(_, { }, { dataSources, correlationId }) {
22 try {
23 const orders = await dataSources.orderDataSource.getOrdersByUserId(userId);
24 return orders ?? ;
25 } catch (error) {
26 if (error instanceof GraphQLError) throw error;
27 throw new GraphQLError('Failed to fetch orders', {
28 extensions: { code: ErrorCodes.SERVICE_ERROR, correlationId }
29 });
30 }
31}
把 4 个空都填上才能检查(还差 4 个)Fill all 4 blanks to check (4 to go)