React Context 完美替代品 Jotai

news2024/11/18 18:36:12

1. 前言

React 的属性透传场景 虽然有很多方式可以实现,但能做到代码写的少、re-render 轻松处理的方式并不多。

而状态管理工具 Jotai 却可以很好的解决这些问题。

image

最近的业务和组件场景里 也在用此方式实现。

2. React Context 的不足

常规解决数据透传通常使用 React 的 Context 来实现,但它却有一些不足的地方:

  • React Context.Provider 嵌套地狱问题。
  • Context value 修改时,触发消费 Context 组件 re-render 的问题(组件引用值未变化时)。

2.1 Provider 嵌套地狱

如下代码所示:

<Context1.Provider value={value1}>
  <Context2.Provider value={value2}>
    <Context3.Provider value={value3}>
      <Context4.Provider value={value4}>
        <Context5.Provider value={value5}>
          <Context6.Provider value={value6}>
            {children}
          </Context6.Provider>
        </Context5.Provider>
      </Context4.Provider>
    </Context3.Provider>
  </Context2.Provider>
</Context1.Provider>

2.2 Context re-render

此问题通常有三种方式来解决:

  1. 将包含多个属性的 Context 剥离成单个 Context 进行使用。但这样会导致 2.1 中嵌套地狱的问题发生。
function Button() {
  let theme = useContext(ThemeContext);  
  return <ExpensiveTree className={theme} />;
}
  1. 将组件剥离成两个,剥离出来的用 React.memo 进行包裹,再消费。
function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; 
  return <ThemedButton theme={theme} />
}

const ThemedButton = memo(({ theme }) => { 
  return <ExpensiveTree className={theme} />;
});
  1. 在组件的 return 中,用 React.useMemo 包裹,将 Context 中消费的值,做为其依赖项。
function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme;  

  return useMemo(() => { 
    return <ExpensiveTree className={theme} />;
  }, [theme])
}

但这些方法,都会在使用时增加一些额外的工作量,变得繁琐和易出错。

3. 状态管理库 Jotai 的优势

刚好 Jotai 的其中一部分优势就能解决 React Context 中的这些问题。

Jotai 可以完美的解决嵌套地狱的问题,及精准 re-render 的问题。

简单写了一个例子,来看一下 Jotai re-render:[codesandbox] jotai 配合 React.memo re-render 测试。

例子中 Jotai 的 Atom 及组件消费关系如下:

image

4. Jotai 核心

Jotai 的学习成本很低,源码也比较易读。核心就三个 API:

  • atom:创建原子相关方法。
  • useAtom:视图关联原子相关方法。
  • Provider:隔离和存储原子实例相关组件。

只需要熟悉这三个 API 的使用就可以轻松上手。

4.1 atom

atom 创建的其实是一份原子描述,并不会进行相关的存储。

可以创建原始原子及派生原子,用法如下:

// primitive atom
function atom<Value>(initialValue: Value): PrimitiveAtom<Value>

// read-only atom
function atom<Value>(read: (get: Getter) => Value | Promise<Value>): Atom<Value>

// writable derived atom
function atom<Value, Update>(
  read: (get: Getter) => Value | Promise<Value>,
  write: (get: Getter, set: Setter, update: Update) => void | Promise<void>
): WritableAtom<Value, Update>

// write-only derived atom
function atom<Value, Update>(
  read: Value,
  write: (get: Getter, set: Setter, update: Update) => void | Promise<void>
): WritableAtom<Value, Update>

  
const primitiveAtom = atom(initialValue)
const derivedAtomWithRead = atom(read)
const derivedAtomWithReadWrite = atom(read, write)
const derivedAtomWithWriteOnly = atom(null, write)

4.2 useAtom

用来将 atom 关联到视图中,形成响应关系。(atom 必须保持引用不变,否则会无限循环)

基本用法:

// primitive or writable derived atom
function useAtom<Value, Update>(
  atom: WritableAtom<Value, Update>,
  scope?: Scope
): [Value, SetAtom<Update>]

// read-only atom
function useAtom<Value>(atom: Atom<Value>, scope?: Scope): [Value, never]

如果“只读”或“只写”,建议直接使用:

  • useAtomValue
  • useSetAtom

4.3 Provider

使用上和 React Provider 很相似,当然底层也是用 React Context.Provider 包装的。

主要用来隔离 atom 实例 及存储 atom 实例用的。

有隔离需求可以包裹在视图中,无隔离需求可以不添加,和 React Context.Provider 一样。

基本用法:

const Provider: React.FC<{
  initialValues?: Iterable<readonly [AnyAtom, unknown]>
  scope?: Scope
}>

5. Jotai 实现原理

下面看一下内部实现的基本原理。

5.1 Provider

Provider 的创建源码:

const ScopeContainerContext = getScopeContext(scope)

return createElement(
  ScopeContainerContext.Provider,
  {
    // 创建 atoms 的 weakMap 等存储,挂载在 Provider 中
    value: scopeContainerRef.current,
  },
  children
)
const ScopeContextMap = new Map<Scope | undefined, ScopeContext>()

export const getScopeContext = (scope?: Scope) => {
  if (!ScopeContextMap.has(scope)) {
    ScopeContextMap.set(scope, createContext(createScopeContainer()))
  }

  return ScopeContextMap.get(scope) as ScopeContext
}
const scopeContainerRef = useRef<ScopeContainer>()
// 从此处可以看到,Provider 仅作为隔离数据使用,后期不会发生任何 value 层面的变化,
// 所有的触发,都由 内部的 某个 atom 的 listeners 去调用渲染
if (!scopeContainerRef.current) {
  // lazy initialization
  const scopeContainer = createScopeContainer(
    // initialValues 仅作用一次,后期的任何变化,将不会有任何效果
    initialValues,
    unstable_createStore
  )
  if (unstable_enableVersionedWrite) {
    let retrying = 0
    scopeContainer.w = (write) => {
      setVersion((parentVersion) => {
        const nextVersion = retrying ? parentVersion : { p: parentVersion }
        write(nextVersion)
        return nextVersion
      })
    }
    scopeContainer.v = version
    scopeContainer.r = (fn) => {
      ++retrying
      fn()
      --retrying
    }
  }
  scopeContainerRef.current = scopeContainer
}

可以清楚的看到,主要就做了:

  • Provider 隔离。
  • 在 Provider 的 value 中挂载 atom 实例。
  • 通过 scope 缓存 createContext 实例。

另外,创建完 Provider 后,未来的所有变化都不会导致其内部的 value 发生变化,所以由 Provider 导致的渲染是不存在的。且 initialValues 也只会消费一次,未来变化也不会响应。

通过 scope 的设置,可以实现和原生 React Context.Provider 一样的效果,进行分割及多层嵌套。

5.2 atom

atom 的生成很简单,就是一份描述:

export function atom<Value, Args extends unknown[], Result>(
  read: Value | Read<Value, SetAtom<Args, Result>>,
  write?: Write<Args, Result>
) {
  const key = `atom${++keyCount}`
  const config = {
    toString: () => key,
  } as WritableAtom<Value, Args, Result> & { init?: Value }
  if (typeof read === 'function') {
    config.read = read as Read<Value, SetAtom<Args, Result>>
  } else {
    config.init = read
    config.read = (get) => get(config)
    config.write = ((get: Getter, set: Setter, arg: SetStateAction<Value>) =>
      set(
        config as unknown as PrimitiveAtom<Value>,
        typeof arg === 'function'
          ? (arg as (prev: Value) => Value)(get(config))
          : arg
      )) as unknown as Write<Args, Result>
  }
  if (write) {
    config.write = write
  }
  return config
}

生成完成后,会挂载在 Provider 的 store 中。store 由 WeakMap 维护,所以不用担心内存泄漏的问题。

store 中的派生 atom 的触发变化和渲染,主要由 dependencieslisteners 实现。

5.3 useAtom

export function useAtomValue<Value>(atom: Atom<Value>, scope?: Scope) {
  const ScopeContext = getScopeContext(scope)
  const scopeContainer = useContext(ScopeContext)
  // ...
}

可以看到 useAtom 中的信息也是通过读取 Context 实现,scope 用来标识与 Provider 匹配。

const [[version, valueFromReducer, atomFromReducer], rerenderIfChanged] =
  useReducer<
    Reducer<
      readonly [VersionObject | undefined, Awaited<Value>, Atom<Value>],
      VersionObject | undefined
    >,
    VersionObject | undefined
  >(
    (prev, nextVersion) => {
      const nextValue = getAtomValue(nextVersion)
      if (Object.is(prev[1], nextValue) && prev[2] === atom) {
        return prev // bail out
      }
      return [nextVersion, nextValue, atom]
    },
    versionFromProvider,
    (initialVersion) => {
      const initialValue = getAtomValue(initialVersion)
      return [initialVersion, initialValue, atom]
    }
  )

基于useAtom 创建的值,由 useReducer 实现,可以有效优化部分 re-render 的问题。

useEffect(() => {
  const { v: versionFromProvider } = scopeContainer
  if (versionFromProvider) {
    store[COMMIT_ATOM](atom, versionFromProvider)
  }
  // Call `rerenderIfChanged` whenever this atom is invalidated. Note
  // that derived atoms may not be recomputed yet.
  const unsubscribe = store[SUBSCRIBE_ATOM](
    atom,
    rerenderIfChanged,
    versionFromProvider
  )
  rerenderIfChanged(versionFromProvider)
  return unsubscribe
}, [store, atom, scopeContainer])
const subscribeAtom = (
  atom: AnyAtom,
  callback: (version?: VersionObject) => void,
  version?: VersionObject
) => {
  const mounted = addAtom(version, atom)
  const listeners = mounted.l
  listeners.add(callback)
  return () => {
    listeners.delete(callback)
    // TODO should version be `undefined` for delAtom?
    delAtom(version, atom)
  }
}

useRender 的 dispatch 会添加到 atom 的订阅列表中,当 atom 发生变化时,会执行订阅列表中的 dispatch 实现响应。

image

6. 开发中的注意

如果 Jotai 使用在应用级的全局状态中,不太需要注意。但在开发的组件中使用 Jotai 还是需要注意的。

6.1 开发组件的注意

最根本的一点就是 atom 的隔离问题,不然组件内的数据用的是同一份,会导致干扰。

所以需要额外包裹 Provider 进行隔离:

const ComponentA = () => {
  return <div>A</div>;
};

export default () => {
  return (
    <Provider>
      <ComponentA />
    </Provider>
  );
};

如果原组件有 memo,需要把它移到 Provider 这一层:

export default memo(() => {
  return (
    <Provider>
      <ComponentA />
    </Provider>
  );
});

如果有 forwardRef 之类的,基本要写两遍:

const ComponentA = forwardRef(({ onChange, defaultValue }, ref) => {
  useImperativeHandle(ref, () => ({ changeStyle }));
  return <div>A</div>;
});

export default memo(
  forwardRef((props, ref) => {
    useImperativeHandle(ref, () => ({
      changeStyle: _ref.current.changeStyle,
    }));

    const _ref = useRef();

    return (
      <Provider>
        <ComponentA ref={_ref} />
      </Provider>
    );
  })
);

🌟 Github 原文地址

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/148169.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

BUUCTF 之 [ACTF2020 新生赛]Exec(命令执行漏洞)

BUUCTF 之 [ACTF2020 新生赛]Exec&#xff08;命令执行漏洞&#xff09;相关观察进攻相关 项目内容难度简单类型WEB靶场BUUCTF坐标Exec观察 这界面和这网页标题结合起来&#xff0c;相信给位都能猜到这个靶场中很有可能存在命令执行漏洞。 进攻 构造如下语句显示当前路径中的…

Learning Monocular Visual Odometry via Self-Supervised Long-Term Modeling

Paper name Learning Monocular Visual Odometry via Self-Supervised Long-Term Modeling Paper Reading Note URL: https://arxiv.org/pdf/2007.10983.pdf TL;DR ECCV 2020 文章&#xff0c;该文章认为在短时间序列上训练无法在长时间序列上良好泛华&#xff0c;所以受到…

从Web3视角审视茅台的“元宇宙”APP,这或是中国版的“星巴克奥德赛”

图片来源&#xff1a;由无界 AI 绘画工具生成2023年1月1日&#xff0c;一款名为《巽风数字世界》的APP登录App Store&#xff0c;这是由茅台和网易联合推出的虚拟世界APP。因而&#xff0c;有媒体称&#xff0c;茅台要进军元宇宙了&#xff01;简单讲&#xff0c;这是一款虚拟世…

Spring核心与设计思想 -- IoC与DI

Spring核心与设计思想 -- IoC与DI一、Spring 是什么&#xff1f;1.1 什么是容器&#xff1f;1.2 什么是 IoC&#xff1f;二、理解 IoC2.1 传统程序开发的问题2.2 分析2.3 控制反转式程序开发2.4 对比总结规律2.5 理解 Spring IoC三、DI 概念说明一、Spring 是什么&#xff1f; …

k8s集群部署springboot项目

一、前言 本篇,我们将基于k8s集群,模拟一个比较接近实际业务的使用场景,使用k8s集群部署一个springboot的项目,我们的需求是: 部署SpringBoot项目到阿里云服务器 ;基于容器打包,推送私有镜像仓库 ;采用K8S集群部署,对外暴露服务,pod副本扩容,公网可以访问 ;二、完…

【案例教程】GEE遥感云大数据在林业中的应用

以Earth Engine&#xff08;GEE&#xff09;为代表全球尺度地球科学数据&#xff08;尤其是卫星遥感数据&#xff09;在线可视化计算和分析云平台应用越来越广泛。该平台存储和同步遥感领域目前常用的MODIS、Landsat和Sentinel等卫星影像、气候与天气、地球物理等方面的数据集超…

禅道的部署与使用

文章目录1、禅道介绍2、下载安装3、使用3.1、创建项目3.2、添加用户3.3、团队管理3.4、测试人员提Bug3.5、开发人员修复Bug4、总结1、禅道介绍 官网地址&#xff1a;https://www.zentao.net禅道是专业的研发项目管理软件&#xff0c;有如下特点&#xff1a; 细分需求、任务、缺…

一篇五分生信临床模型预测文章代码复现——Figure 4-6 临床模型构建(七)

之前讲过临床模型预测的专栏,但那只是基础版本,下面我们以自噬相关基因为例子,模仿一篇五分文章,将图和代码复现出来,学会本专栏课程,可以具备发一篇五分左右文章的水平: 本专栏目录如下: Figure 1:差异表达基因及预后基因筛选(图片仅供参考) Figure 2. 生存分析,…

Redis简介与使用

Redis简介与使用:数据保存在内存。&#xff08;mysql数据保存在硬盘&#xff09;是一个 高性能的key-value数据库NoSQL&#xff08;非关系数据库&#xff09;和SQL数据库&#xff08;关系数据库&#xff09;比较&#xff1a;适用场景不同:sql数据库适合用于关系特别复杂的数据查…

Java8-

https://blog.51cto.com/u_14014612/5677262 Stream到底是什么呢&#xff1f; 是数据渠道&#xff0c;用于操作数据源&#xff08;集合、数组等&#xff09;所生成的元素序列。 “集合讲的是数据&#xff0c;Stream讲的是计算&#xff01;” 流的构成 当我们使用一个流的时…

贴片SD卡移植FATFS文件系统

一、前言 手头有一块深圳雷龙的 SDIO 接口的贴片 SD 卡&#xff0c;挂载个文件系统把玩一下。首先介绍一下使用的贴片 SD 卡&#xff0c;以前使用的都是拔插式的 SD 卡&#xff0c;下面这个样子&#xff1a; 拿到的贴片雷龙 SD 卡如下图&#xff1a; 二、贴片 SD 卡介绍 雷龙的…

RAD Studio 11.2 详细图文安装教程 (delphi 11.2)

准备 准备好安装镜像和嘿嘿工具 镜像可以到官网下载&#xff0c;工具百度搜一搜就能找到 系统环境&#xff1a;win11 专业版 开始安装 双击镜像文件&#xff0c;然后将里面的文件复制到一个地方 注意一定要把这两个文件复制出来再运行&#xff0c;否则会嘿嘿失败 右键点击红色…

前有农民歌唱家大衣哥,后有农民女歌手草帽姐,都给山东人增光了

山东自古以来是孔孟之乡礼仪之邦&#xff0c;也诞生了很多的好儿女&#xff0c;农民歌唱家大衣哥就是其中的一位。大衣哥是从《星光大道》走红&#xff0c;也是大山东人的骄傲&#xff0c;他出名十几年不忘本&#xff0c;扎根农村为老乡们做贡献的精神&#xff0c;也感动了无数…

从0到1完成一个Vue后台管理项目(五、登录页,mock,token,axios封装)

往期 从0到1完成一个Vue后台管理项目&#xff08;一、创建项目&#xff09; 从0到1完成一个Vue后台管理项目&#xff08;二、使用element-ui&#xff09; 从0到1完成一个Vue后台管理项目&#xff08;三、使用SCSS/LESS&#xff0c;安装图标库&#xff09; 从0到1完成一个Vu…

两大js调试工具的安装

首先要确保已经安装nodejs&#xff0c;具体安装步骤可以百度。 node-inspect工具的安装和使用 对于node-inspect工具我们可以全局安装&#xff1a; npm install node-inspect -g由于安装过cnpm&#xff0c;所以几乎在1秒内将node-inspect安装完成。 cnpm的安装方法&#xff…

Multisim14.0仿真应用举例——OCL电路输出功率和效率的研究

安装教程&#xff1a;http://t.csdn.cn/qvvqA一、理论讲解OCL全称: Output Capacitorless&#xff0c;中文名称:无输出电容的功率放大电路。功率放大带电路最重要的技术指标是最大输出功率Pom和效率η 。若输入信号Ui为正弦信号sin(wt)&#xff0c;负载上能得到的最大输出电压且…

你是真的“C”——详解C语言实现静态版通讯录

详解C语言实现静态版通讯录&#x1f60e;前言&#x1f603;通讯录设计的关键思想点分析 &#x1f64c;通讯录界面&#xff08;meun&#xff09;设计 &#x1f64c;增加信息功能实现代码&#x1f64c;删除信息功能实现代码&#x1f64c;查询信息功能实现代码&#x1f64c;修改信…

【强训】Day04

努力经营当下&#xff0c;直至未来明朗&#xff01; 文章目录一、选择二、编程1. 计算糖果2. 进制转换答案1. 选择2. 编程普通小孩也要热爱生活&#xff01; 一、选择 下列与队列结构有关联的是&#xff08;&#xff09; A 函数的递归调用 B 数组元素的引用 C 多重循环的执行…

【年终总结系列 2022】魔幻、疲惫、收获与成长

时光飞逝&#xff0c;转眼到了2022年的年末了。仿佛昨天还在看冬奥的开幕式&#xff0c;而第一次做核酸又似乎很遥远&#xff0c;生活像是一瞬间恢复了喧嚣&#xff0c;可能是三年之期已到吧。这一年于我们大众而言我愿称之为魔幻的一年&#xff0c;于我自己而言&#xff0c;则…

6.4 工具-RabbitMQ

目录 6.4.1 RabbitMQ概述 6.4.1.1 什么是RabbitMQ 6.4.1.2 Erlang和AMQP 6.4.1.3 RabbitMQ 6.4.1.3.1 RabbitMQ优点 6.4.1.3.2 应用解耦 6.4.1.3.3 异步处理 6.4.1.3.4 流量削峰 6.4.2 RabbitMQ安装 6.4.3 RabbitMQ架构 6.4.3.1 架构图 6.4.3.1.1 术语 6.4.3.1.…