第十章 案例 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的示例。

相关推荐
byzh_rc5 分钟前
[微机原理与系统设计-从入门到入土] 微型计算机基础
开发语言·javascript·ecmascript
m0_471199635 分钟前
【小程序】订单数据缓存 以及针对海量库存数据的 懒加载+数据分片 的具体实现方式
前端·vue.js·小程序
编程大师哥7 分钟前
Java web
java·开发语言·前端
A小码哥8 分钟前
Vibe Coding 提示词优化的四个实战策略
前端
Murrays8 分钟前
【React】01 初识 React
前端·javascript·react.js
大喜xi12 分钟前
ReactNative 使用百分比宽度时,aspectRatio 在某些情况下无法正确推断出高度,导致图片高度为 0,从而无法显示
前端
helloCat12 分钟前
你的前端代码应该怎么写
前端·javascript·架构
电商API_1800790524712 分钟前
大麦网API实战指南:关键字搜索与详情数据获取全解析
java·大数据·前端·人工智能·spring·网络爬虫
康一夏14 分钟前
CSS盒模型(Box Model) 原理
前端·css
web前端12314 分钟前
React Hooks 介绍与实践要点
前端·react.js