第十章 案例 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,并讨论 全局状态模式 以 结束本书。

相关推荐
翻滚吧键盘14 分钟前
vue绑定一个返回对象的计算属性
前端·javascript·vue.js
苦夏木禾18 分钟前
js请求避免缓存的三种方式
开发语言·javascript·缓存
超级土豆粉26 分钟前
Turndown.js: 优雅地将 HTML 转换为 Markdown
开发语言·javascript·html
乆夨(jiuze)1 小时前
记录H5内嵌到flutter App的一个问题,引发后面使用fastClick,引发后面input输入框单击无效问题。。。
前端·javascript·vue.js
小彭努力中1 小时前
141.在 Vue 3 中使用 OpenLayers Link 交互:把地图中心点 / 缩放级别 / 旋转角度实时写进 URL,并同步解析显示
前端·javascript·vue.js·交互
小飞悟2 小时前
前端高手才知道的秘密:Blob 居然这么强大!
前端·javascript·html
code_YuJun2 小时前
Promise 基础使用
前端·javascript·promise
Codebee2 小时前
OneCode自主UI设计体系:架构解析与核心实现
前端·javascript·前端框架
邢同学爱折腾2 小时前
当前端轮播图遇上Electron: 变身一款丝滑的 图片查看器
javascript·electron
guojl2 小时前
深度解决大文件上传难题
架构