T3 Code 非官方中文翻译不是官方产品,也不是 T3 Tools 官方文档。源提交 d2c25950b4d9英文原文GitHub 源文件
Skip to content

Web 界面 ​

基础组件 ​

控件来自 components/ui。 AGENTS.md 说明怎么使用和扩展它们。

不要把原生的 <button>、<input>、<select> 或 <textarea> 做成基础组件已经提供的那种控件的样子。元素本来就不是那种控件时,用原生元素才对:语义行、标签页、调整大小的手柄、色块、编辑器表面,以及任何行为或几何形状按设计就不一样的东西。

样式 ​

在渲染该元素的组件里,用 Tailwind 类写样式。

  • 全局 CSS,无论在 index.css 还是其他样式表里,只用于所属组件上的类表达不了的东西:生成的 Markdown 或命令式 DOM、自定义元素和 shadow root、动画、运行时主题变量,以及浏览器或 Electron 的集成。
  • 类已经能覆盖的静态值,不要用 style={{ ... }},也不要写行内的 <style>。运行时算出来的值可以用。
  • 只跟主题有关的规则用 @variant dark 和 @variant light,不用 .dark 选择器。

组合 ​

Composer(编写器)的横幅是参照。

  • ComposerBanner.tsx 导出一个由若干小槽位组成的对象。槽位自己持有类名,接收 className,并展开其余 props,用 cn 合并类名。变体和密度是 Root 上的 props。需要控件的槽位渲染 Button。
  • ComposerBannerStack.tsx 组合这些槽位,自己只负责堆叠行为:优先级、展开和收起、关闭时的过渡。
  • 调用方只有几行槽位,横幅样式由槽位自己带着:ComposerPlanFollowUpBanner.tsx。

新界面照这样做。去组合或扩展组件,不要复制它的标记和类名。某一处调用重复了另一处已经在用的处理方式,就把它做成共享组件上的变体或槽位。新的 Composer 横幅或通知,使用 ComposerBanner 的槽位。

大型组件里的处理函数 ​

V8 让同一次渲染中创建的每个闭包共用一个作用域。被记忆化的处理函数会留住创建它的那次渲染的作用域,而这个作用域还持有这次渲染里其他被记忆化的值。同一个组件里的处理函数如果在不同时间发生变化,每一个都会留着更早的作用域,里面是更早的处理函数;于是旧的渲染,连同其中的服务器配置和对话线程(thread)数据,会在组件保持挂载的整段时间里留在内存中。React Compiler 记忆化出来的闭包也是如此。

在 ChatView 和 ChatComposer 这类又大、又长期存在的组件里,把处理函数写成普通函数,只在它们离开组件的地方用 useStableCallback 包起来:props、hook 的依赖数组,以及 effect 或 memo 的函数体。组件内部的调用继续用这个普通函数,这样在 await 之后接着做的工作,仍然作用在它出发的那次渲染上。子组件在渲染期间调用的函数,或者在 await 之后调用的函数,来自一个按输入记忆化的模块级工厂,因此它的闭包只保留这些输入。

React Compiler ​

编译器只有在能证明代码遵守 React 的规则时,才会记忆化一个组件或 hook。它证明不了的任何写法,例如在渲染期间读取 ref,或在 effect 里调用 setState,都会让它跳过整个函数,而且构建不会报错。Lint 把每一次跳过报成 react(...) 警告,附注是 "React Compiler skipped optimizing this component or hook"。

apps/web/src 下的每个文件都必须能编译。Lint 在这里把这些规则定为错误,并且 t3code/no-manual-memo 会拒绝 useMemo 和 useCallback,因为编译器已经让它们变得多余;上游并没有哪条规则直接禁止它们。它也会拒绝 "use memo"。当一个值在某次 hook 调用的两侧都进了普通函数调用时,编译器不会给出缓存作用域。"use memo" 辅助函数能强行得到一个,但会把顺序问题藏起来。要改的是顺序:对该值的普通使用放在 hook 之前,或者把辅助函数内联。不调用任何 hook 的 use* 辅助函数会被当成普通函数编译,什么也不缓存,所以计算派生值的辅助函数应当自己去读来源 hook,而不是把这个值当作参数接进来。t3code/no-hookless-use-function 会拒绝不调用任何 hook 的 use* 函数。Lint 会漏掉一些放弃优化的情况,尤其是带 finally 的 try,以及 try 内部的 throw,因此 reactCompilerCoverage.test.ts 会编译每一个文件,只要有跳过就失败。异步处理函数很少需要 try:一条 atom 命令(command)会落定成一个结果,所以清理可以跟在 await 后面。如上所述,useStableCallback 仍然会标记那些离开大型组件的处理函数。

非官方译文 · 源提交 d2c25950b4d9 · 不代表 T3 Tools