Pressidian
花园入口
笔记
项目
关于
实验室
GitHub
花园入口
笔记
项目
关于
实验室
GitHub

KNOWLEDGE PATHS

笔记库
当前位置
笔记库/前端/项目笔记/代达罗斯/考试/2026-07-29-Anatomy列表状态流

EXAM-20260729-01 阅卷报告:Anatomy 列表状态流

12 分钟阅读 · Note

目录树 578 篇

              • EXAM-20260729-01 阅卷报告:Anatomy 列表状态流
              • EXAM-20260729-01:Anatomy 列表状态流
            • 代达罗斯前端考试索引
            • 前端能力画像
            • 深色模式
            • 阅卷报告:CrateDetailPageContent
            • 阅卷报告:ThemeProvider.tsx(第二次)
            • CrateDialog
          • 项目待做
          • 性能优化
          • UI设计
      • 前端技术栈
    • 笔记目录
    • CLAUDE.md
    • Vue 组件与 Render 函数

关联笔记 6

↗useMemo的安全数组正文关联↗EXAM-20260729-01:Anatomy 列表状态流同一路径↗深色模式共同主题↗阅卷报告:CrateDetailPageContent共同主题↗阅卷报告:ThemeProvider.tsx(第二次)共同主题↗CrateDialog共同主题
  • EXAM-20260729-01 阅卷报告:Anatomy 列表状态流

EXAM-20260729-01 阅卷报告:Anatomy 列表状态流

总览

  • 考生:Dano Day
  • 得分:63 / 90(70%)
  • 评级:🌕🌕🌕🌑🌑 — 及格,有需改进之处
  • 致命问题:3 个
  • Git 标准答案基准:0e800f1d0b7ced0a105b0a8ae3df3acd76b84072
  • 试卷文件:
    • apps/app/src/hooks/use-debounce.ts
    • apps/app/src/pages/AnatomiesPage/AnatomiesCreateDialog.tsx
    • apps/app/src/pages/AnatomiesPage/AnatomiesPageContent.tsx
    • apps/app/src/pages/AnatomiesPage/AnatomiesPage.tsx

本次不对翻译 key、是否使用 t(...)、直接文案、复杂 className 还原度或布局视觉差异扣分。按钮名称、错误/加载/空状态、列表 key、事件边界和可操作性仍按功能评分。

成绩汇总

题号文件知识点得分错误类型主要结论
1use-debounce.ts泛型 Hook、副作用清理9/10洁癖功能与依赖正确,仅有缩进、分号和尾随空格
2AnatomiesCreateDialog.tsx表单校验、提交边界3/10功能空名称仍提交,且提交值缺少必需的 structure
3AnatomiesCreateDialog.tsx受控输入、可访问表单8/10模式/洁癖标签、alert、按钮完整;输入未绑定 value,存在注释 import 与题目标记 typo
4AnatomiesPageContent.tsxURL 与本地状态同步7/10功能/模式基本同步成立;未把空白搜索归一为 undefined,未即时重置本地页码
5AnatomiesPageContent.tsxRefine 列表查询、安全派生值8/10模式filters、分页和总页数正确;缺少稳定空数组回退,memo 依赖过宽
6AnatomiesPageContent.tsxmutation 参数与生命周期6/10功能创建分支正确;归档 values 应为 { id } 而非字符串
7AnatomiesPageContent.tsx路由、事件边界、分页6/10功能/模式路由和分页基本正确;行点击未排除按钮,且两个 Hook 依赖不完整
8AnatomiesPageContent.tsx异步 JSX、列表交互、可访问性6/10功能/洁癖状态顺序进步明显;搜索未受控、错误无重试、按钮冒泡、分页未禁用、option 缺 key
9AnatomiesPage.tsx路由适配层10/10—Props 映射和组件职责完全正确
合计63/9070%

逐题批改

第1题:实现可清理的泛型防抖 Hook — 9/10

知识点:泛型 Hook、Effect 清理、依赖数组。错误类型:代码洁癖。

考生核心代码:

const [debouncedValue, setDebouncedValue] = useState(value)

useEffect(() => {
  const timer = setTimeout(() => {
    setDebouncedValue(value)
  }, delay)

  return () => clearTimeout(timer)
}, [value, delay])

return debouncedValue

标准答案核心:

const [debounced, setDebounced] = useState(value);
useEffect(() => {
  const timer = setTimeout(() => setDebounced(value), delay);
  return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
  • 功能:状态、延迟更新、旧定时器清理和依赖数组全部正确。
  • 模式:泛型输入与输出保持一致,没有闭包时序问题。
  • 洁癖:第25行有尾随空格;缩进和分号不统一,但不影响行为。
  • 改进:提交前运行 formatter 或 git diff --check,这题即可满分。

第2题:校验并提交 Anatomy 草稿 — 3/10

知识点:早返回、Schema 输入边界、表单规范化。错误类型:功能错误。

考生核心代码:

setError(null)
const name1 = name.trim()
if (name1.length === 0) setError("name length is 0")
onCreate({ name: name1, purpose })

标准答案核心:

if (!name.trim()) {
  setError(/* 必填提示 */);
  return;
}
const draft = createEmptyAnatomyDraft(name.trim(), purpose.trim());
onCreate(draft as unknown as Record<string, unknown>);
  • 功能:空名称会设置错误,但没有 return,仍然继续提交;这是上次“互斥分支缺少提前结束”的同类错误。
  • 功能:createEmptyAnatomyDraft 还会生成 structure.schemaVersion/defaultPolicies/root。只提交 { name, purpose } 会缺少创建 Schema 的必需结构。
  • 模式:useCallback 依赖正确;名称修剪正确,但 purpose 未修剪。
  • 洁癖:name1 可改成语义更明确的 trimmedName。
  • 改进:校验失败路径先终止,再由共享工厂生成完整领域对象,不要手写一个“看起来差不多”的部分对象。

第3题:组合可访问的创建对话框 — 8/10

知识点:输入标签、错误语义、UI 组件复用、受控输入。错误类型:模式与洁癖。

考生核心代码:

{error && <div role="alert">{error}</div>}
<Label htmlFor="name">name</Label>
<Input id="name" onChange={handleNameChange} />
<Label htmlFor="purpose">purpose</Label>
<Textarea id="purpose" onChange={handlePurposeChange} />
<Button type="button" onClick={handleSave}>submit</Button>
<Button type="button" onClick={handleClose}>close</Button>

标准答案关键行为:

<input value={name} onChange={handleNameChange} />
<textarea value={purpose} onChange={handlePurposeChange} />
{error && <div>{error}</div>}
  • 功能:错误条件渲染、role="alert"、Label 关联、取消和提交操作都存在;使用项目 UI 组件是正确方向。
  • 模式:Input/Textarea 缺少 value,目前是非受控输入。输入仍能触发状态,但外部状态变化或清理时 UI 不会同步。
  • 洁癖:保留了注释 import,题目标记被改成 文件 2/{4},并有多项 Props 顺序 lint 错误。
  • i18n:直接使用 Create/name/purpose/submit/close 不扣分。
  • 改进:JSX 不需要复刻复杂样式,但数据绑定三件套 id + value + onChange 应完整。

第4题:双向同步 URL、筛选状态与防抖搜索 — 7/10

知识点:URL 单一数据源、防抖时序、Effect 依赖。错误类型:功能边界与模式。

考生核心代码:

if (prevDebouncedRef.current !== debouncedSearch) {
  void routeNavigate({
    to: ".",
    search: { ...searchParams, search: debouncedSearch, page: undefined },
  })
  prevDebouncedRef.current = debouncedSearch
}

标准答案核心:

const value = debouncedSearch.trim() || undefined;
if (value !== searchParams.search) {
  void routeNavigate({
    to: ".",
    search: { ...searchParams, search: value, page: undefined },
  });
  setPage(1);
}
  • 功能:外部 search/status/page 回填和防抖后导航都已实现。
  • 功能:输出搜索词没有 trim,也没有把空字符串转为 undefined;URL 会保留 search=。
  • 功能:导航清除了 URL page,但没有同时 setPage(1),新路由回填前可能短暂用旧页查询。
  • 模式:lint 报告 searchParams 与 routeNavigate 缺失依赖。标准实现为避免 searchParams 对象引发循环进行了有意识的抑制;你的版本没有解释该取舍。
  • 改进:先完成“规范化 → 与 URL 比较 → 导航 → 本地同步”四步,ref 只负责判断是否处理过输入值。

第5题:建立筛选条件与安全的 Refine 列表查询 — 8/10

useMemo的安全数组 知识点:Refine useList、条件数组、稳定派生值。错误类型:模式问题。

考生核心代码:

const filters = useMemo(() => {
  const result = [];
  if (search) result.push({ field: "search", operator: "contains", value: search });
  if (status) result.push({ field: "status", operator: "eq", value: status });
  return result;
}, [search, status]);

const { result, query } = useList<AnatomySummary>({
  pagination: { currentPage: page, pageSize: PAGE_SIZE },
  resource: ResourceName.anatomies,
  filters,
});
const anatomies = useMemo(() => result?.data, [result]);

标准答案核心:

const anatomies = useMemo(
  () => result?.data ?? [],
  [result?.data],
);
const total = result?.total ?? 0;
  • 功能:两个筛选条件、分页、资源、total 和 totalPages 都正确;TypeScript 未在本题报告类型错误。
  • 模式:缺少 ?? [],没有兑现“始终提供安全数组”的接口;依赖整个 result 比依赖 result?.data 更宽。
  • 疑问:这里的 useMemo 不是为了减少昂贵计算,而是为了让“暂无数据时的空数组”保持稳定引用。若没有下游依赖引用稳定性,完全可以写 const anatomies = result?.data ?? [],或使用模块级 EMPTY_ARRAY。total 是 number 原始值,不存在数组/对象引用不断变化的问题,因此无需 memo。
  • 改进:不要为了 useMemo 而 useMemo;先判断下游是否依赖引用稳定性。

第6题:完成创建与归档 mutation 生命周期 — 6/10

知识点:普通 mutation、自定义 mutation、响应导航。错误类型:功能错误。

考生创建代码与标准行为等价:

createRecord(
  { resource: ResourceName.anatomies, values: _values },
  { onSuccess: /* 关闭并按响应 id 导航 */ },
)

归档代码:

archiveRecord({
  url: AnatomyCustomAction.archive,
  values: _id,
  method: "post",
}, { onSuccess: () => query?.refetch?.() })

标准答案:

archiveRecord({
  url: AnatomyCustomAction.archive,
  method: "post",
  values: { id },
}, { onSuccess: () => query?.refetch?.() });
  • 功能:创建请求、成功关闭、安全读取 id 和详情导航均正确。
  • 功能:归档 payload 错误。archiveAnatomyInputSchema 明确要求 { id: string },字符串会在 data provider 的 Zod parse 阶段被拒绝。
  • 模式:_values 和 _id 实际被使用,不应使用下划线前缀。
  • 疑问:useCustomMutation 的第一个参数描述 action 身份和 payload。Hook 使用字段名 values,data provider 内部把它交给 custom 的 payload,再由该 action 对应的 Zod schema 校验。最快确认方法是看 AnatomyCustomAction.archive 的 data provider 分支和相邻测试,而不是猜对象形状。
  • 疑问:router 被回调闭包读取,所以必须写进依赖数组。即使当前 router 引用通常稳定,Hook 规则也要求把外部读取值声明出来,避免未来实现变化形成陈旧闭包。

第7题:维护弹窗、详情导航与分页边界 — 6/10

知识点:路由适配、事件冒泡、分页边界、Hook 依赖。错误类型:功能与模式。

考生行点击代码:

const handleRowClick = useCallback(
  (_event, id) => handleNavigateDetail(id),
  [handleNavigateDetail],
);

标准答案核心:

const target = event.target as HTMLElement;
if (target.closest("button")) return;
handleNavigateDetail(id);
  • 功能:关闭弹窗、清理 create 参数、详情路由、上一页和下一页计算基本正确;下一页主动 clamp 到 totalPages 是合理增强。
  • 致命问题:行点击没有排除 button。点击 Archive 会同时冒泡到行,既归档又进入详情页。
  • 模式:handleCloseDialog 展开了完整 searchParams,但依赖只写 searchParams.create;handleNextPage 使用 totalPages,却未把它放进依赖数组。
  • 疑问:Route.useNavigate() 是当前文件路由生成的类型化导航器,擅长 to: "." 和当前路由 search 更新;useRouter().navigate() 来自全局 router 实例,适合明确的跨路由绝对目标。这里局部筛选用 routeNavigate,跳详情用 router.navigate,职责不同。
  • 改进:行可点击且内部还有按钮时,优先在行处理器里用 closest("button") 做统一防线;也可在操作区 stopPropagation,二选一至少要可靠存在。[event.target.closest()](../../../../浏览器&网络/浏览器环境/Web API/事件/event.target.closest().md)

第8题:组合完整的 Anatomy 列表异步界面 — 6/10

知识点:异步状态机、受控筛选、列表 key、交互边界、可访问性。错误类型:功能与洁癖。

考生状态骨架:

query.isLoading ? <LoadingState />
  : query.isError ? <div>ERROR</div>
  : anatomies.length <= 0 ? <EmptyState />
  : anatomies.map(/* rows */)

标准状态骨架:

query.isLoading ? <LoadingState />
  : query.isError ? <ErrorState action={/* refetch */} />
  : anatomies.length === 0 ? <EmptyState />
  : anatomies.map(/* rows with key and isolated actions */)
  • 进步:这次已经严格排对 loading → error → empty → data,列表行也有稳定 key;这是相对上次最明确的提升。
  • 功能:搜索 Input 没有 value,清筛选时输入框不会跟随状态清空,URL 初始搜索也不会显示。
  • 功能:清筛选按钮只判断 searchParams.status,仅有搜索词时不会出现。
  • 功能:错误态有可理解文本,但没有重试入口。
  • 功能:Archive 按钮总是显示,已归档记录仍可再次操作;上一页/下一页按钮没有 disabled 状态。
  • 接口:第7题的按钮冒泡问题在本题真实发生;已声明的 handleStopPropagation 未接入 JSX。
  • 洁癖:status option 缺 key;map 的 index 未使用;有空 div、未使用 import、20xp 拼写错误。
  • 样式:没有复刻复杂 grid、颜色或像素值不扣分;20xp 仅作为无效 class 的清理问题记录。
  • 改进:JSX 先守住五件事:状态顺序、value/onChange、map key、按钮 disabled、事件隔离。视觉类名放在这些行为之后。

第9题:用路由参数组合页面内容组件 — 10/10

知识点:路由包装层、Props 映射、职责分离。错误类型:无。

考生代码:

const { page, search, status } = Route.useSearch();
return (
  <AnatomiesPageContent
    initialPage={page}
    search={search}
    status={status}
  />
);
  • 功能:三个参数映射完全正确。
  • 模式:没有在路由包装层复制本地状态或数据请求,职责边界清楚。
  • 洁癖:空行较多,但未构成功能或 lint 问题,不扣分。

多文件数据流与接口一致性

Route search → AnatomiesPage → AnatomiesPageContent → useDebounce/useList 的主链路已接通;AnatomiesCreateDialog → onCreate → handleCreate → useCreate → 详情路由 的 Props 和回调类型也一致。

真正的接口断点有两个:Dialog 交付的是不完整草稿,无法满足 Anatomy 创建 Schema;归档 mutation 交付字符串而不是 { id },无法通过 action Schema。交互层还有一个断点:列表行点击与行内归档按钮没有隔离。

疑问解答

1. void routeNavigate(...) 是做什么的?

导航通常返回 Promise。前面加 void 表示“我明确选择忽略这个返回值”,也能满足部分 no-floating-promises 规则。它不会捕获失败,也不会让操作更安全;需要根据结果继续处理时使用 await,需要错误反馈时使用 await 或 .catch(...)。

2. 第4题的防抖 URL 逻辑哪里需要修?

顺序应是:修剪防抖值 → 空字符串变 undefined → 与当前 URL search 比较 → 不同时导航并清 page → 同步本地 page 为 1。这样既不保留 search=,也不会为同值重复导航。

3. 为什么 anatomies 用 useMemo,total 不用?

不是因为 result?.data 计算很重,而是空数组是对象,每次写 ?? [] 都可能产生新引用。若数组会进入 Effect、memo 子组件或其他依赖,稳定引用有价值。number 是原始值,result?.total ?? 0 不会制造对象引用问题。你认为“计算本身不重”是正确的;没有引用稳定需求时可以不用 useMemo。

4. 为什么 router 是 useCallback 依赖?

回调函数读取了外层 router,所以 router 属于闭包依赖。依赖数组不是只列“会变化的业务状态”,而是列回调从外部读取的值。它当前可能稳定,但显式声明能避免未来陈旧闭包。

5. 自定义 mutation 的 values 怎么确认?

沿着 AnatomyCustomAction.archive → dataProvider.custom → archiveAnatomyInputSchema 查。这个 schema 是 { id: z.string() },所以 values 必须是 { id }。相邻的 dataProvider.test.ts 也直接展示了合法 payload。

6. routeNavigate 和 router.navigate 的区别?

Route.useNavigate() 绑定当前路由,类型推导更了解当前 search schema,适合相对导航和局部 URL 状态;useRouter().navigate() 是全局 router 实例方法,适合带绝对路径和 params 的跨页面导航。两者底层都走 TanStack Router,但类型上下文和使用范围不同。

验证结果

检查结果说明
bun run check-types失败8 个错误;6 个在答卷范围,其中 useTranslation 未使用按规则不扣分;另 2 个来自试卷外既有文件
目标文件 oxlint失败14 个 error、12 个 warning;关键项为 Hook 依赖、按钮冒泡相关未消费回调、option key 和未使用项
bun run test部分通过17 个文件中 14 个通过;61/65 测试通过,4 个失败均位于试卷外文件
dataProvider.test.ts通过10/10,通过测试证明归档 action 需要对象 payload
git diff --check失败use-debounce.ts 有 1 行尾随空格;另有 LF/CRLF 提示
调试遗留扫描通过没有 console.log、数字测试标签或测试颜色类;仍有注释 import、空 div 和未使用项

致命问题

  1. 创建流程断裂:空名称仍提交,合法名称又缺少 AnatomyDraftInput 必需的 structure。
  2. 归档 mutation 把字符串作为 payload,无法通过 { id: string } Schema 校验。
  3. 点击列表内 Archive 会冒泡触发行导航,导致归档动作同时进入详情页。

三大改进方向

  1. 把“失败路径必须终止”和“领域对象由共享工厂创建”设为提交边界检查项。
  2. 自定义 mutation 不猜 values;固定沿 action 常量、data provider 分支、Zod schema、测试四处追类型。
  3. JSX 先完成行为骨架:受控值、四态分支、列表 key、disabled、事件隔离;样式最后处理。

下次考试的 JSX 教学模式

根据本次反馈,后续试卷固定调整为:

  1. JSX 保留约 70%–85% 骨架,仅挖掉关键条件、数据绑定、事件回调、key、disabled 和可访问属性,不再整段清空。
  2. className 只独立考常用布局:flex/grid、items-*、justify-*、gap-*、常见间距、w-full、truncate、disabled:*。
  3. 生僻内容如任意值 grid、复杂像素、主题颜色、组合选择器全部保留,并在旁边注释“它控制什么”,不要求背诵。
  4. 每道 JSX 题增加一小段“边做边学”:先解释当前骨架,再只完成一个关键行为,最后用 2 个可观察结果自检。
  5. 一场考试只安排一题中等 JSX 综合题,其余 JSX 题拆成短小局部题,避免体力消耗掩盖真实知识点。

下一次建议训练

下一场优先用较小组件复测三件事:表单失败分支早返回、自定义 mutation 的 Zod payload、可点击行与内部按钮的事件隔离。异步四态顺序已经明显进步,改为保留骨架的复习题,不再要求从空白重写整段 JSX。