React Tracked(react-tracked.js.org)是一个用于状态使用追踪的库,它能基于属性访问自动优化重新渲染。它提供了类似Valtio的减少额外重新渲染的功能。
React Tracked 可以与 其他状态管理库 混用。它主要是和 useState
和 useReducer
混用,也可以和 Redux
,Zustand
和 其他类似的库 混用。
在本章中,我们将再次讨论通过状态使用追踪来优化重新渲染的问题,并对比相关库。我们会学习 React Tracked 的两种用法。第一种用法是结合常用的 useState
和 useReducer
,另一种用法是 结合 React Redux。最后,我们将了解 React Tracked 如何与未来版本的 React 协同工作。
在本章,我们会讨论这几个主题:
- 理解 React Tracked
- 结合
useState
、useReducer
使用 React Tracked - 结合 React Redux 使用 React Tracked
- 未来展望
理解 React Tracked
我们学习了几个全局状态管理库,但 React Tracked 和我们之前 学习的 状态管理库 还是有细微不同的。React Tracked 并没有 提供 修改 状态的 函数,但它却提拱了一个渲染优化功能。我们称呼这个功能为 状态使用追踪。
让我们回顾一下React Context的 工作原理。因为 React Tracked 的 状态使用追踪的 气质一个 用例,变身React 的 Context。
假设我们用 createContext 创建了 一个 Context,如下:
js
const NameContext = createContext([
{ firstName: 'react', lastName: 'hooks' },
() => {}
])
createContext 接收 了 一个 类型为 数组 的 初始值。这个数组里 的 第一个 值 是 初始化的 状态对象。第二个参数是一个没有实际功能的函数。
至于为啥要把这两个值放进数组里,则是为了匹配useState的返回值。我们经常用useState来定义 NameProvider ,用作全局状态:
js
const NameProvider = ({ children }) => {
<NameContext.Provider
value=(
useState({ firstName: 'React', lastName: 'hooks' })
)
>
{children}
</NameContext.Provider>
}
你可以在 跟组件使用 这个 NameProvider
组件,也可以在临近调用组件值的地方使用它。
现在,我们有了NameProvider
组件,我们可以在其 子树下使用其值。我们通过useContext
使用其值。假设我们要设计一个只使用firstName
的 useFirstName
钩子:
js
const useFirstName = () => {
const [{ firstName }] = useContext(NameContext);
return firstName;
};
这段代码运行良好。然而,这段代码也会发生 额外重新渲染。如果我们只更新 lastName
而不更新 firstName
,那么这个 新的 Context 值 会被传导到,而 useContext
会 触发一个重新渲染。但是,useFirstName
只读取firstName
。因此,这就导致了额外的重新渲染。
如果只是追求实现功能,这段代码是没问题的。但是如果要追求极致的性能体验,这段代码还不够尽善尽美,我们应该避免firstName
以外的Context的值引发的重新渲染。
而 状态更新收集 可以实现这一行为。我们可以通过使用proxy,确保只在 firstName
变化时,才触发重新渲染。
React Tracked 为我们 提供了一个 叫 useTracked
的钩子。useTracked
是 useContext
的替代品。useTracked
会包裹住状态,并追踪其 使用状态。一般而言,人们这么使用 useTracked
:
js
const useFirstName = () => {
const [{ firstName }] = useTracked();
return firstName;
}
这段代码看起来和 useContext
没什么不同。而这这是 状态 使用 追踪的 要点。我们的代码看起来 和 往常一样。而在幕后,它会追踪 状态的 使用情况,并自动优化重新渲染。
React Tracked 所 使用的 自由渲染优化 特性,和 Valtio是 同一款,它们都来自 公共库 proxy-comparegithub.com/dai-shi/pro...。
在这一个小节,我们学习了 状态使用追踪,以及 React Tracked 是 如何 优化 重新渲染的。在下一个小节,我们要讨论 如何 结合 useState
、 useReducer
使用 React Tracked。
结合 useState
、 useReducer
使用 React Tracked
React Tracked 的 主要目的,就是 要替代 React的 Context。
我们要讨论 React Tracked 与 useState的 组合,以及 React Tracked 与 useReducer的组合。我们先讨论 React Tracked 与 useState的 组合。
React Tracked 与 useState的 组合
在讨论 如 将 React Tracked 与 useState的 组合 使用时,我们先 回顾一下 如何用 React Context 来 创建 全局状态。
我们可以定义一个 用于useState来传递初始值的 自定义钩子:
js
const useValue = () => {
useState({ count: 0, text: "hello" })
}
用TS来定义自定义钩子是非常好的,因为你可以通过typeof 操作符来推断返回类型。
以下为Context的定义:
js
const StateContext = createContext<
ReturnType<typeof useValue> | null
>(null)
为了更便捷地使用Context,我们可以定义一个Provider组件。下面是一个使用了useValue的Provider组件:
js
const Provider = ({ children }: { children: ReactNode }) => {
<StateContext.Provider value={useValue()}>
{children}
</StateContext.Provider>
}
这是一个注入了 StateContext.Provider
的组件.
为了使用Context 的值,我们可以使用 useContext
。我们可以定义一个这样的自定义钩子:
js
const useStateContext = () => {
const contextValue = useContext(StateContext);
if (contextValue === null) {
throw new Error("Please use Provider");
}
return contextValue;
}
这个钩子会首先检查 contextValue 的非空性。如果 contextValue 不存在,会抛出相关错误。
接下来,我们要定义 用于计数的 Counter 组件:
js
const Counter = () => {
const [state, setState] = useStateContext();
const inc = () => {
setState((prev) => ({
...prev,
count: prev.count + 1
}));
};
return (
<div>
count: {state.count}
<button onClick={inc}>+1</button>
</div>
)
}
注意,useStateContext
返回了由 值 和 更新函数组成的 元组。而这个元组的类型,正切合了 useValue
的返回值。
接下来,我们要定义用于输入 文本的 TextBox
组件:
js
const TextBox = () => {
const [state, setSate ] = useStateContext();
const setText = (text: string) => {
setState((prev) => ({...prev, text})
}
return (
<div>
<input
value={state.text}
onChange={(e) => setText(e.target.value)}
/>
</div>
)
}
我们再次使用了useStateContext
钩子,获得了 state值 以及 其 更新 函数。这个setText
函数以string作为参数,并触发setState
函数。
最后,我们需要定义 App
组件:
js
const App = () => {
<Provider>
<div>
<Counter />
<Counter />
<TextBox />
<TextBox />
</div>
</Provider>
}
那么,这个组件会如何运行呢?这个Context会把 这个 状态对象当作一个整体来处理,并且,当这个状态对象发生变化时,useContext
会触发重新渲染。即便是单一的属性发生变化, 所有的useContext
钩子都会触发重新渲染。也就是说,当我们点击了Counter
组件的按钮,体会给状态对象的count属性加一,但会触发 Coutner
组件和TextBox
组件都重新渲染。尽管只是count变化了而text没有变化,TextBox
也要随之重新渲染。我们称呼这为额外重新渲染。
在使用Context时,这类额外重新渲染是可预期的。如果我们希望避免这个现象,我们可以把它拆分为更多小的Context。
那么,如果我们想用React Tracked来避免 不必要的 重新渲染,该怎么办?为了达到这个目的,我们需要使用createContainer
:
js
import { createContainer } from "react-tracked";
之后:
js
const { Provider, useTracked } =
createContainer(useValue)
从结果中提取了Provider
和useTracked
。Provider
组件的使用方式与本节前一个示例中的用法相同。useTracked
钩子的使用方式与我们在本节前一个示例中定义的useStateContext
钩子相同。
使用了useTracked
钩子后,我们的Counter
组件变成这样了:
js
const Counter = () => {
const [state, setState] = useTracked();
const inc = () => {
setState(
(prev) => ({ ...prev, count: prev.count +1 })
);
};
return (
<div>
count: {state.count}
<button onClick={inc}> +1 </button>
</div>
);
};
我们仅仅是把useStateContext
换成了useTracked
。剩下的代码都是一样的。
同理,新的TextBox
组件是 这样的:
js
const TextBox = () => {
const [state, setSate ] = useTracked();
const setText = (text: string) => {
setState((prev) => ({...prev, text})
}
return (
<div>
<input
value={state.text}
onChange={(e) => setText(e.target.value)}
/>
</div>
);
};
至于App
组件,除了Provider
是新的组件,其他都是一样的:
js
const App = () => {
<Provider>
<div>
<Counter />
<Counter />
<TextBox />
<TextBox />
</div>
</Provider>
}
这个新应用的表现如何?由useTracked
返回的状态对象会被追踪,这意味着useTracked
钩子会记住状态的哪些属性被访问过。只有当被访问的属性发生变化时,useTracked
钩子才会触发重新渲染。因此,如果你点击Counter
组件中的按钮,只有Counter
组件会重新渲染,而TextBox
组件不会重新渲染,如下所示:

本质上,我们不过是把createContext
换成了 createContainer
,把useStateContext
换成了 useTracked
。这样为我们优化了重新渲染。这就是状态使用追踪功能。
我们传递给createContainer
函数的自定义钩子useValue
可以是任何内容,只要它返回类似useState
的元组。下面来看另一个使用useReducer
的示例。