在React 19中,@eslint-react/no-unstable-default-props和 @eslint-react/no-unstable-context-value这两个规则仍然相关吗?
我注意到ESLint的 React规则 no-unstable-default-props 和 no-unstable-context-value 在默认情况下是禁用的。它们似乎是有用的规则,关于通过使用 'use memo' 或 useMemo() 来避免不必要的重新渲染。
2024年 12月发布的React 19引入了 React Compiler,它会自动处理记忆化。不过,useMemo 的文档页面对于在何种情形下它仍然相关并不清楚。它只是说React Compiler “减少了手动 useMemo 调用的需要”,然后按旧版React的文档列出所有 useMemo 的用例。在网上搜索时,我发现 useMemo “很少需要”,但具体何时仍然不清楚。
那么,在会触发 “no-unstable-default-props” 和 “no-unstable-context-value” 规则的情形下,useMemo 仍然需要吗?
解决方案
这些规则仍然相关,但应被理解为“引用稳定性”规则,而不是“总是添加 useMemo” 的规则。
React 19本身并不会改变JavaScript对象的身份(引用)。这仍然会在每次渲染时创建一个新的数组:
function List({ items = [] }) {
// items is a new array whenever the prop is omitted
}
这也会在每次渲染时创建一个新的对象:
<MyContext.Provider value={{ foo: "bar" }}>
{children}
</MyContext.Provider>
这正是这些规则所警告的点。no-unstable-default-props 规则明确指出,数组/对象/函数的默认值在解构被评估时会每次创建新的标识,并建议改用稳定的引用。no-unstable-context-value 规则同样针对上下文的问题:当提供者的值改变时,消费者会重新渲染,而一个新鲜的字面对象会让值在每次渲染时看起来像是改变了。
因此,在没有使用React Compiler的情况下,旧的建议仍然是正确的:
const EMPTY_ITEMS: Item[] = [];
function List({ items = EMPTY_ITEMS }) {
// ...
}
以及:
function MyProvider({ children }) {
const value = useMemo(() => ({ foo: "bar" }), []);
return (
<MyContext.Provider value={value}>
{children}
</MyContext.Provider>
);
}
然而,开启React Compiler时,答案会改变。React的文档表示编译器在构建时自动优化React代码,并减少对手动 useMemo、useCallback 和 React.memo 的需求。useMemo 的文档也说同样的事情:编译器会自动对值和函数进行记忆化,减少对手动 useMemo 调用的需要。
关键在于,编译器并不是因为你安装了React 19就被启用。它是一个构建阶段的工具,你需要安装/配置,例如通过 babel-plugin-react-compiler。React的官方文档也描述了渐进式采用模式,包括仅对带注解的函数进行编译,使用 "use memo"。
这也是为什么这些规则并非简单地“无用”。它们在以下情形仍然有用:
- 未启用编译器;
- 文件/组件未包含在编译采用范围内;
- 由于不受支持的代码或React规则违规,编译器跳过了某个组件;
- 你正在编写库代码,可能被不对你的源代码进行编译的应用所使用;
- 你希望对一些超出React渲染优化范围的东西保持稳定的引用,例如Effect依赖项或第三方API。
规则文档本身现在也承认这一点。no-unstable-default-props 将这视为在组件使用React Compiler的 "use memo" 指令时可以的:
function MyComponent({ items = [] }: MyComponentProps) {
"use memo";
return null;
}
规则文档指出,在React 19中,如果组件使用 "use memo",编译器会自动优化默认属性的引用稳定性,因此字面默认值是可以接受的。
同样,no-unstable-context-value 将此视为有效:
function MyComponentProvider() {
"use memo";
return (
<MyContext.Provider value={{ foo: "bar" }}>
<ExampleConsumer />
</MyContext.Provider>
);
}
因为编译器可以为已编译的组件稳定化该值。
因此,实际结论是:
- 如果你没有使用React Compiler,保留这些规则或手动稳定化这些值。
- 如果你在所有地方都使用React Compiler,且你的lint工具能够理解
"use memo",这些规则的重要性会降低,可能会显得太过冗杂。 - 如果你以增量方式使用编译器,请保持启用,或至少保留在未被编译的代码部分。
另外,useMemo 并非在这些规则触发的所有情形下都是“必需”。对于默认属性,比较简单的修复通常是一个模块作用域的常量:
const EMPTY_ARRAY: string[] = [];
function Component({ items = EMPTY_ARRAY }) {
// ...
}
对于上下文值,useMemo 仍然是一个普通的非编译修复:
function Provider({ children }) {
const [user, setUser] = useState(null);
const value = useMemo(() => ({ user, setUser }), [user]);
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}
但在已编译的代码中,直接将对象字面量写在内联也可以,前提是组件确实编译成功。
最后一个警告:React Compiler并不能在语义上替代每一个可能的 useMemo。React的编译器文档表示 useMemo 和 useCallback 仍然可以作为兜底方案,当你需要对某些值(尤其是用于Effect依赖项的值)进行精确控制时。编译器也并不会全局对每一个任意函数进行记忆化;它的记忆化是针对组件和钩子,且不会在不相关的组件之间共享。