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

React Tracked(react-tracked.js.org)是一个用于状态使用追踪的库,它能基于属性访问自动优化重新渲染。它提供了类似Valtio的减少额外重新渲染的功能。

React Tracked 可以与 其他状态管理库 混用。它主要是和 useStateuseReducer 混用,也可以和 ReduxZustand 和 其他类似的库 混用。

在本章中,我们将再次讨论通过状态使用追踪来优化重新渲染的问题,并对比相关库。我们会学习 React Tracked 的两种用法。第一种用法是结合常用的 useStateuseReducer,另一种用法是 结合 React Redux。最后,我们将了解 React Tracked 如何与未来版本的 React 协同工作。

在本章,我们会讨论这几个主题:

  • 理解 React Tracked
  • 结合 useStateuseReducer 使用 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使用其值。假设我们要设计一个只使用firstNameuseFirstName 钩子:

js 复制代码
const useFirstName = () => {
    const [{ firstName }] = useContext(NameContext);
    return firstName;
};

这段代码运行良好。然而,这段代码也会发生 额外重新渲染。如果我们只更新 lastName 而不更新 firstName,那么这个 新的 Context 值 会被传导到,而 useContext 会 触发一个重新渲染。但是,useFirstName只读取firstName。因此,这就导致了额外的重新渲染。

如果只是追求实现功能,这段代码是没问题的。但是如果要追求极致的性能体验,这段代码还不够尽善尽美,我们应该避免firstName以外的Context的值引发的重新渲染。

而 状态更新收集 可以实现这一行为。我们可以通过使用proxy,确保只在 firstName 变化时,才触发重新渲染。

React Tracked 为我们 提供了一个 叫 useTracked 的钩子。useTrackeduseContext 的替代品。useTracked会包裹住状态,并追踪其 使用状态。一般而言,人们这么使用 useTracked

js 复制代码
const useFirstName = () => {
    const [{ firstName }] = useTracked();
    return firstName;
}

这段代码看起来和 useContext 没什么不同。而这这是 状态 使用 追踪的 要点。我们的代码看起来 和 往常一样。而在幕后,它会追踪 状态的 使用情况,并自动优化重新渲染。

React Tracked 所 使用的 自由渲染优化 特性,和 Valtio是 同一款,它们都来自 公共库 proxy-comparegithub.com/dai-shi/pro...

在这一个小节,我们学习了 状态使用追踪,以及 React Tracked 是 如何 优化 重新渲染的。在下一个小节,我们要讨论 如何 结合 useStateuseReducer 使用 React Tracked。

结合 useStateuseReducer 使用 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)

从结果中提取了ProvideruseTrackedProvider组件的使用方式与本节前一个示例中的用法相同。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的示例。

相关推荐
每天都想睡觉的1900几秒前
Vue 的 keep-alive 详解:作用、问题与优化
前端·vue.js
curdcv_po几秒前
🫴为什么看大厂的源码,看不到undefined,看到的是void 0
前端
就是我1 分钟前
Electron多窗口应用实战
前端·javascript·electron
芝士加4 分钟前
最全301/302重定向指南:从SEO到实战,一篇就够了
前端·javascript·面试
若梦plus4 分钟前
React19 状态管理方案与原理剖析
前端·react.js
陈随易5 分钟前
2025年100个产品计划之第7个(树图) - 目录结构生成工具
前端·后端·程序员
Joomla中文网6 分钟前
掌握Joomla 4/5自定义库开发:实现PSR-4规范与无缝自动加载
前端
若梦plus6 分钟前
Zustand 使用优化:深入探讨状态管理性能提升
前端·react.js
Cache技术分享11 分钟前
96. Java 数字和字符串 - 比较字符串和字符串的各个部分
前端·后端