第十章 案例 4 - React Tracked 【下】

结合 useReducer 使用 React Tracked

在这个例子中,我们要用 useReducer 来替代 useStateuseReducer是一个语法稍微复杂一些,但功能更加丰富的钩子。

这次我们实现的useValue钩子由useReduceruseEffect来实现。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>
}

这个应用的行为 和 前一个是完全一样的。其主要不同在于,基于useStateuseReduceruseValue的 返回值不同。基于useReduceruseValue 返回的 是 一个由 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,并讨论 全局状态模式 以 结束本书。

相关推荐
江城开朗的豌豆1 分钟前
JavaScript篇:移动端点击的300ms魔咒:你以为用户手抖?其实是浏览器在搞事情!
前端·javascript·面试
华洛8 分钟前
聊聊我们公司的AI应用工程师每天都干啥?
前端·javascript·vue.js
江城开朗的豌豆8 分钟前
JavaScript篇:你以为事件循环都一样?浏览器和Node的差别让我栽了跟头!
前端·javascript·面试
技术小丁11 分钟前
使用 HTML +JavaScript 从零构建视频帧提取器
javascript·html·音视频
gyx_这个杀手不太冷静11 分钟前
Vue3 响应式系统探秘:watch 如何成为你的数据侦探
前端·vue.js·架构
漫谈网络29 分钟前
TypeScript 编译 ES6+ 语法到兼容的 JavaScript介绍
javascript·typescript·es6
bin91531 小时前
DeepSeek 助力 Vue3 开发:打造丝滑的日历(Calendar),日历_天气预报日历示例(CalendarView01_18)
前端·javascript·vue.js·ecmascript·deepseek
eternal__day1 小时前
微服务架构下的服务注册与发现:Eureka 深度解析
java·spring cloud·微服务·eureka·架构·maven
江城开朗的豌豆1 小时前
JavaScript篇:反柯里化:让函数'反悔'自己的特异功能,回归普通生活!
前端·javascript·面试
江城开朗的豌豆1 小时前
JavaScript篇:数字千分位格式化:从入门到花式炫技
前端·javascript·面试