结合 useReducer
使用 React Tracked
在这个例子中,我们要用 useReducer
来替代 useState
。 useReducer
是一个语法稍微复杂一些,但功能更加丰富的钩子。
这次我们实现的useValue
钩子由useReducer
和useEffect
来实现。useReducer
由一个 reducer 函数 和 一个初始值来定义。useEffect
里有一个用于在控制台打印状态的函数。如下是useValue
基于TS的实现代码:
js
const useValue = () => {
type State = { count: number; text: string };
type Action =
| { type: "INC" }
| { type: "SET_TEZT"; text: string }
const [state, dispatch] = useReducer(
(state: State, action: Action) => {
if (action.type === "INC") {
return { ...state, count: state.count + 1 };
}
if (action.type === "SET_TEXT") {
return { ...state, text: action.text }
}
}
,
{ count: 0, text: "hello"}
);
useEffect(() => {
console.log("latest state", state);
}, [state]);
return [state, dispatch] as const;
}
这个reducer函数接受 INC 操作类型 和 SET_TEXT 操作类型。而useEffect
用于在控制台打印相关变动,当然,这个钩子还可以做更多的事情。这个useValue
最后返回了一个由 state 和 dispatch 组成的 元组。只要返回的元组遵循这种形式,我们就可以按自己的喜好实现这个钩子。例如,我们可以使用多个useState钩子。
为了使用这个新的useValue
钩子,我们还要运行createContainer
:
js
const { Provider, useTracked } = createContainer(useValue);
我们使用createContainer
的方式没有变,即便我们用了新的useValue
。
再使用useTracked
钩子,我们就可以使用Counter
组件了:
js
const Counter = () => {
const [state, dispatch] = useTracked();
const inc = () => dispatch({ type: "INC"});
return (
<div>
count: {state.count}
<button onClick={inc}> +1 </button>
</div>
)
}
然后,是TextBox
组件:
js
const TextBox = () => {
const [state, dispatch] = useTracked();
const setText = (text: string) => {
dispatch({ type: "SET_TEZT", text})
}
return (
<div>
<input
value={state.text}
onChange={(e) => setText(e.target.value)}
/>
</div>
);
};
最后,我们要实现 App
组件:
js
const App = () => {
<Provider>
<div>
<Counter />
<Counter />
<TextBox />
<TextBox />
</div>
</Provider>
}
这个应用的行为 和 前一个是完全一样的。其主要不同在于,基于useState
和 useReducer
的 useValue
的 返回值不同。基于useReducer
的 useValue
返回的 是 一个由 state 和 dispatch 组成的 元组;因此,useTracked
返回的 也是 由 state 和 dispatch 组成的 元组。
React Tracked 可以 优化重新渲染的原因 有很多。除了状态使用追踪 特性外,React Tracked 内部 还使用了 use-context-selector库。它允许我们基于一个selector函数来订阅 Context值。而这种订阅方式,绕开了React Context的限制。
值这一小节,我们学习了一个只有纯React Context的例子,一个用 useState
实现的React Tracked用例,和一个用useReducer
实现的React Tracked用例。在下一小节,我们学习 结合 React Redux 使用 React Tracked,而在这个场景中,是没有 使用 use-context-selector的。
结合 React Tracked 使用 React Tracked
React Tracked 主要是用来取代 React 的 Context的。而这个功能,是由 use-context-selector 在内部实现的。
React Tracked 提供了一个 名为 createTrackedSelector
函数来 处理 非 React Conext的使用场景。它接收了一个名为 useSelector
的参数,并返回了 useTrackedState
:
js
const useTrackedState = createTrackedSelector(useSelector);
而useSelector
是一个以选择函数为参数,并返回选择结果的钩子。它会在返回结果发生变化时,触发重新渲染。而 useTrackedState
则是 返回 了一个 用 proxy 来包裹整个 状态 的 钩子。而这个proxy,可以追踪状态的使用。
我们以一个具体的React Redux例子展开下面的内容。
首先,我们从 redux,react-redux 和 react-tracked 引入 一些函数:
js
import { createStore } from "redux";
import {
Provider,
useDispatch,
useSelector,
} from "react-redux";
import { createTrackedSelector } from "react-tracked";
前两个引入,是使用 React Redux的 常见代码。而第三个引入,是针对 react-trakced的。
接下来,我们基于 initialState
和 reducer函数来定义 一个 Redux 仓库:
js
type State = { count: number; text: string };
type Action =
| { type: "INC" }
| { type: "SET_TEXT"; text: string };
const initialState: State = { count: 0, text: "hello" };
const reducer = (state = initialState, action: Action) => {
if (action.type === "INC") {
return { ...state, count: state.count + 1 };
}
if (action.type === "SET_TEXT") {
return { ...state, text: action.text };
}
return state;
}
const store = createStore(reducer)
这是常规的创建 Redux 仓库的 代码。目前还没有使用到 React Tracked。
从 react-redux 引入的 createTrackedSelector
,允许我们通过 传入 useSelector
钩子的方式 创建 useTrackedState
钩子:
js
const useTrackedState =
createTrackedSelector<State>(useSelector);
我们需要通过<State>
来指明类型。
使用了useTrackedState
后,Counter
组件是这样的:
js
const Counter = () => {
const dispatch = useDispatch();
const { count } = useTrackedState();
const inc = () => dispatch({ type: "INC" });
return (
<div>
count: {count} <button onClick={inc}>+1</button>
</div>
);
};
除了useTrackedState
这一行,这段代码其他地方和常规的React Redux代码一样。如果要把useTrackedState
替换成常规的 React Redux代码,是这样的:
js
const count = useSelector((state) => state.count);
使用或者不使用 React Tracked,看起来变化不大。但是,如果 使用 useSelector
,开发者需要有意识的写不会触发额外重新渲染的选择函数。而使用 useTrackedstate
的话,这个钩子可以帮助我们自动优化重新渲染。
同理,TextBox
组件可以这样实现:
js
const TextBox = () => {
const dispatch = useDispatch();
const state = useTrackedState();
const setText = (text: string) => {
dispatch({ type: "SET_TEXT", text});
}
return (
<div>
<input
value={state.text}
onChange={(e) => setText(e.target.value)}
/>
</div>
)
}
为了优化重新渲染,我们用useTrackedState
来替代useSelector
,以达到自动优化重新渲染的目的。为了更好的理解自动渲染优化的作用,我们可以假设TextBox
组件接收了一个showCount
属性,而这个 属性用于决定是否展示 count
属性。我们可以这样改动代码
js
const TextBox = ({ showCount }: { showCount: boolean }) => {
const dispatch = useDispatch();
const state = useTrackedState();
const setText = (text: string) => {
dispatch({ type: "SET_TEXT", text});
}
return (
<div>
<input
value={state.text}
onChange={(e) => setText(e.target.value)}
/>
{showCount && <span>{state.count}</span>}
</div>
)
}
注意,我没有改动useTrackedState
那一行。如果只是使用useSelector
,实现同样的功能会复杂很多。
最后,我们要实现App
组件:
js
const App = () => (
<Provider store={store}>
<div>
<Counter />
<Counter />
<TextBox />
<TextBox />
</div>
</Provider>
);
在注入store这一点上,使用 React Tracked 与否都是一样的。而使用了自动渲染优化,则意味着,点击递增按钮时,只会重新渲染Counter
组件,而TextBox
组件不会重新渲染,如下:

在这个小节,我们学习了如何 在 React Redux 里使用 React Tracked。接下来,我们要展望 React和 React Tracked 的未来。
展望未来
React Tracked 的 实现,是基于两个内部库
- proxy-compare
- use-context-selector
正如我们在本章学到的,有两个方法可以使用 React Tracked。第一个方式,是通过 React Context 使用createContainer
; 第二个方式,是通过 React Redux 使用 createTrackedSelector
。而基础函数,是基于 proxy-compare 的 createTrackedSelector
。而 createContainer
是 基于 createTrackedSelector
和 use-context-selector 实现的,更高级的抽象函数。
在React Tracked 使用 Context 时,use-context-selector 库 是 非常重要的。那 use-context-selector 库 重要在哪里呢?它提供了一个叫useContextSelector
的库。如我们在第三章所学,React Context的子组件全都会在value 更新时进行重新渲染。而针对这一现象,有人提议优化Context 的能力 - useContextSelector
。而 use-context-selector 这个库,虽然是 使用者们开发的库,但是已经最大程度封装出 useContextSelector
的 功能了。
在笔者书写此文时,还不确定 React官方团队 是否 会 接纳这一提议,但在 未来 React 官方团队 也行会实现 useContextSelector
钩子,或者其他类似的 方法。在这种情况下, React Tracked 可以 轻松地 从 use-context-selector 迁移到 useContextSelector
。希望 React Tracked 可以 良好的 兼容 React 的新特性。
在React Tracked 中 抽象出 use-context-selector,有助于 未来的版本迁移。如果React 未来 发布了 useContextSelector
,React Tracked 可以在 不 改变公共API 的情况下进行 迁移。在这种实现设计下,createTrackedSelector
可以理解为 React Tracked 的块 函数,createContainer
是 React Tracked 是 胶水函数。导出这两个函数,为我们提供了这两种能力。
在这个小节,我们讨论了React Tracked 的 实现设计,以及它如何与未来兼容。
章节概要
在这一章,我们学习了React Tracked。这个库有两个作用,第一个是替代 React 的 Context;另一个是 提示 诸如 React Redux 这类库的 选择函数的性能。
本质上而言,React Tracked 并不是一个全局状态库。它需要结合 useState
, useReducer
, 或者 Redux 使用。React Tracked 为它们提供的,是自动优化重新渲染的能力。
在下一章,我们要比较三个全局状态管理库:Zustand,Jotai 和 Valtio,并讨论 全局状态模式 以 结束本书。