React 状态管理: Recoil - Facebook 状态管理

news/2024/7/19 17:56:58 标签: react.js, facebook, javascript, 状态管理

React 状态管理: Recoil - Facebook 状态管理

文章目录

  • React 状态管理: Recoil - Facebook 状态管理
  • Recoil 概念
  • Recoil 示例
    • 0. RecoilRoot
    • 1. Atom 状态
    • 2. Selector 导出状态
    • 3. Async Selector 异步导出状态
    • 4. Atom Family 状态类
    • 5. Selector Family 导出状态类
    • 6. RecoilBridge: 跨 Root 通信
    • 小结
  • 参考连接
  • 完整代码示例

Recoil 概念

第一次尝试 Recoil 让我非常惊艳,以前我都是用 redux 进行状态管理,但是多余的重新渲染总是让人不胜其扰,Recoil 可以说是彻底解决 redux 基于 Context API 进行状态管理的弊端,并借助了 React hooks 的优势,将状态更新推迟到一个个最接近底部的子节点上。

上一张图大家就能够感受到差异了(借用 Dave McCabe 大佬的图)

相当于是把每一个状态设计成一个单独的可订阅对象,然后使用 hooks 进行订阅最终状态更新就只会修改直接使用该状态的组件,同时避免了 Redux 将多个状态都堆放在同一个对象上造成使用同一个 Context 的组件触发多余的渲染

Recoil 示例

下面我们透过实际操作来体验一下 Recoil 的便捷

0. RecoilRoot

下面 1-5 的例子我们都统一使用单一个 RecoilRoot 作为全局作用域,不论是 atom 还是 selector 都是基于 RecoilRoot 来获取当前值,对于不同 RecoilRoot 则会产生不同的状态实例,切记。

1. Atom 状态

第一个概念是状态,Recoil 定义了一个 atom 函数来声明一个状态,使用如下

  • /src/state/states.ts
export const counterState = atom({
  key: 'counter',
  default: 0,
});

接下来在组件内我们就可以像原来使用 useState 一样来使用这个状态,只需要简单改成使用 useRecoilState 即可

useRecoilStateuseState 类似,会返回一个 value 与一个 setter,Recoil 更进一步提供我们只需要其中一个的时候的钩子 useRecoilValueuseSetRecoilState

第一个组件我们使用 useRecoilValue 来获取状态

  • /src/components/Counter.tsx
import React from 'react';
import { useRecoilValue } from 'recoil';

import { counterState } from '@/state/states';

const Counter = () => {
  const count = useRecoilValue(counterState);

  return (
    <>
      <h2>Atom - Counter</h2>
      <div>
        <h3>Counter in ComponentA</h3>
        <div>count: {count}</div>
      </div>
    </>
  );
};

export default Counter;

第二个组件我们则是透过 button 提供修改的入口

  • /src/components/CounterController.tsx
const CounterController = () => {
  const [count, setCount] = useRecoilState(counterState);

  const increment = () => {
    setCount(count + 1);
  };

  const reset = useResetRecoilState(counterState);

  const setCount2 = useSetRecoilState(counterState);
  const addTen = () => setCount2(count + 10);

  return (
    <CounterControllerWrapper>
      <h3>Counter controller</h3>
      <div>
        <button onClick={increment}>+1</button>
        <button onClick={addTen}>+10</button>
        <button onClick={reset}>Reset</button>
      </div>
    </CounterControllerWrapper>
  );
};

export default CounterController;

我们可以看到,useRecoilState 的 setter 与 useSetRecoilState 返回的 setter 是等价的

2. Selector 导出状态

第二个概念非常有趣,与 vue 的 computed 非常相似,甚至还能透过 set 反过来更新依赖状态

首先我们先定义一个摄氏温度做为原始状态

  • /src/state/states.ts
export const celsiusState = atom({
  key: 'celsius',
  default: 32,
});

接下来定义华氏温度与摄氏温度同步

  • /src/state/selectors.ts
export const fahrenheitState = selector<number>({
  key: 'fahrenheit',
  get: ({ get }) => (get(celsiusState) * 9) / 5 + 32,
  set: ({ set }, newValue: number) =>
    set(celsiusState, ((newValue - 32) * 5) / 9),
});

get 方法透过内部的钩子函数来获取摄氏温度的状态,同时可选的 set 方法还能够反过来更新摄氏温度的状态

不管是 atom 还是 selector 其实都是返回一个 RecoilState 对象,所以在使用上是一致的

  • /src/components/Temperature.tsx
const Temperature = () => {
  const [celsius, setCelsius] = useRecoilState(celsiusState);
  const [fahrenheit, setFahrenheit] = useRecoilState(fahrenheitState);

  const onCelsiusChange = (e) => {
    const val = Number(e.target.value);
    console.log(`new celsius ${val}`);
    setCelsius(val);
  };
  const onFahrenheitChange = (e) => {
    const val = Number(e.target.value);
    console.log(`new fahrenheit ${val}`);
    setFahrenheit(val);
  };

  return (
    <TemperatureWrapper>
      <h2>Selector - Temparature</h2>
      <div
        style={{
          display: 'flex',
          justifyContent: 'center',
          alignItems: 'center',
        }}
      >
        <label>
          <div>celsius</div>
          <input type="text" value={celsius} onChange={onCelsiusChange} />
        </label>{' '}
        ={' '}
        <label>
          <div>fahrenheit</div>
          <input type="text" value={fahrenheit} onChange={onFahrenheitChange} />
        </label>
      </div>
    </TemperatureWrapper>
  );
};

export default Temperature;

不论输入哪一侧都能够同步两侧的温度

3. Async Selector 异步导出状态

透过上述的 selector 发现,他可以组合、选择、过滤任意多个状态进行导出,甚至作者还提供了异步的方式,让我们能够产生异步的状态。而这个异步状态则是被设计与 Suspense 协同工作的

  • /src/state/selectors.ts
export interface BusinessCard {
  name: string;
  message: string;
}

export const businessCardState = selector<BusinessCard>({
  key: 'businessCard',
  get: async ({ get }) => {
    const [res] = await Promise.all([
      fetch('/businessCard.json'),
      new Promise((resolve) => setTimeout(resolve, 2000)),
    ]);
    return res.json();
  }
});

这里我们加上了一个 2s 的延迟,确保组件不会产生闪烁

  • /src/components/AsyncSelector.tsx

使用的时候则需要透过组件将异步 selector 进行隔离

const BusinessCard = () => {
  const businessCard = useRecoilValue(businessCardState);

  return <div>data: {JSON.stringify(businessCard)}</div>;
};

const AsyncSelector = () => {
  return (
    <div>
      <h2>Async Selector</h2>
      <Suspense fallback={<div>Loading business card ...</div>}>
        <BusinessCard />
      </Suspense>
    </div>
  );
};

效果如下

感觉前面这三种已经足够满足大部分的使用场景了,同时也设计的足够简单,几乎可以一些现有业务进行无痛迁移(如果是使用 redux 那很不好意思了hhh)

4. Atom Family 状态类

下面更是出现所谓的 family 类型,使用下来的感觉就像是定义一个 atom 类型,然后根据参数来获取对应的状态。有点像是当你需要多个相同 atom 的时候,把它变成一个保存了 key:value 的对象,而使用时则是调用并传入对应的 key,返回 atom 实例的感觉

下面我们一样透过计数器来举例,只是这次我们将一次创造多个计数器

  • /src/state/states.ts
export const counterFamily = atomFamily({
  key: 'counterFamily',
  default: 0,
});

使用的时候则是调用 counterFamily(key) 来获取具体的 atom 实例

  • /src/components/CounterItem.tsx
const CounterItem: FC<CounterItemProps> = ({ id }) => {
  const counterState = counterFamily(id);
  const [count, setCount] = useRecoilState(counterState);
  const resetCount = useResetRecoilState(counterState);

  return (
    <div>
      <h2>Atom Family - Counter Item({id})</h2>
      <div>count: {count}</div>
      <div>
        <button onClick={() => setCount(count + 1)}>+1</button>
        <button onClick={() => setCount(count + 10)}>+10</button>
        <button onClick={resetCount}>reset</button>
      </div>
    </div>
  );
};

然后我们一次创建多个对象

  • /src/App.tsx
{Array.from({ length: 3 }, (_, i) => (
  <CounterItem key={i} id={i} />
))}

我们可以看到,三个组件都使用了相同的 atomFamily,但是基于不同的 id 产生了不同的状态实例

5. Selector Family 导出状态类

family 对于 selector 也是类似的,不同的 key 映射到不同的 selector 实例

首先定义基础状态

  • /src/state/states.ts
export const baseNumberState = atom({
  key: 'baseNumberState',
  default: 1,
});

然后定义公用的导出状态类型

  • /src/state/selectors.ts
export const multipliedNumberFamily = selectorFamily<number, number>({
  key: 'multipliedNumber',
  get:
    (multiplier: number) =>
    ({ get }) =>
      get(baseNumberState) * multiplier,
  set:
    (multiplier: number) =>
    ({ set }, newValue: number) =>
      set(baseNumberState, newValue / multiplier),
});

这里我们可以提供任意倍数来获取不同的导出状态量

  • /src/components/MultipliedSelector.tsx
const MultipliedSelector = () => {
  const base = useRecoilValue(baseNumberState);
  const [tenTimes, setTenTimes] = useRecoilState(multipliedNumberFamily(10));
  const [hundredTimes, setHundredTimes] = useRecoilState(
    multipliedNumberFamily(100)
  );

  return (
    <div>
      <h2>Selector Family - Multiplier</h2>
      <div>base: {base}</div>
      <div>
        <h3>Multiplier - x10</h3>
        <input
          type="text"
          value={tenTimes}
          onChange={(e) => setTenTimes(Number(e.target.value))}
        />
      </div>
      <div>
        <h3>Multiplier - x100</h3>
        <input
          type="text"
          value={hundredTimes}
          onChange={(e) => setHundredTimes(Number(e.target.value))}
        />
      </div>
    </div>
  );
};

这里我们使用了一个 10 倍、一个 100 倍的导出状态,两个状态都是基于相同的 baseNumber 状态,因此下面不论修改哪个状态,三个数字都会同步保持倍数关系

6. RecoilBridge: 跨 Root 通信

最后我们回过头来谈谈 RecoilRoot 的问题,由于 atom 和 selector 都是基于 RecoilRoot 来保存状态,但是嵌套的 RecoilRoot 又会遮蔽掉外层的 RecoilRoot,造成我们非常难以划分 RecoilRoot 的界限,最后被迫只能全部写在同一个 RecoilRoot 之下。

未来或许可以透过为 RecoilRoot 附加 ignoreState 的方式来支持内部作用域访问外部 RecoilRoot 的方式。

官方目前则是透过提供 useRecoilBridgeAcrossReactRoots 的方式来支持跨 Root 访问

  • /src/components/NestedRoot.tsx

首先我们先定义一个最外层的 RecoilRoot

const NestedRoot: FC = () => {
  return (
    <RecoilRoot>
      <NestedRootWrapper>
        <Outer />
      </NestedRootWrapper>
    </RecoilRoot>
  );
};

然后 Outer 组件内第一次访问状态

const useRandomNumber = (): [number, () => void] => {
  const [num, setNum] = useRecoilState(randomNumberState);
  const change = () => setNum(Math.random());

  return [num, change];
};

const Outer = () => {
  const [num, change] = useRandomNumber();
  const RecoilBridge = useRecoilBridgeAcrossReactRoots_UNSTABLE();

  return (
    <div className="outer">
      <h2>Outer State: {num}</h2>
      <button onClick={change}>change</button>
      <RecoilRoot>
        <Inner>
          <RecoilBridge>
            <Inner inBridge />
          </RecoilBridge>
        </Inner>
      </RecoilRoot>
    </div>
  );
};

并透过 useRecoilBridgeAcrossReactRoots_UNSTABLE 钩子来将当前的 RecoilRoot 保存下来。后面我们使用两次 Inner 组件,内部的 Inner 使用 RecoilBridge 包裹

const Inner: FC<{ inBridge?: boolean }> = ({ inBridge, children }) => {
  const [num, change] = useRandomNumber();

  return (
    <div className={inBridge ? 'bridge' : 'inner'}>
      <h2>Inner State: {num}</h2>
      <button onClick={change}>change</button>
      {children}
    </div>
  );
};

就能看到最内层的 Inner 与 Outer 所在的 Root 是相同的状态,而中间的 Inner 则是一个独立的作用域

小结

Recoil 作为继 Redux、MobX 之后的一个全新的状态管理库,与前两种的状态管理理念从根本上的不同,感觉还是与 React Hooks 非常搭的,同时提供 Async Selector 为我们开启了全新的概念,将异步远程数据纳入状态管理的范畴。

不过作为一个比较新的库还是存在许多不足,版本上也是还没完全稳定 API 的 0.x.x 版本


参考连接

TitleLink
Recoilhttps://recoiljs.org/
Recoil: State Management for Today’s React - Dave McCabe aka @mcc_abe at @ReactEurope 2020https://www.youtube.com/watch?v=_ISAA_Jt9kI
关于Recoil的atom跨RecoilRoot交互的二三事https://juejin.cn/post/7004754412501467173

完整代码示例

https://github.com/superfreeeee/Blog-code/tree/main/front_end/react/react_recoil


http://www.niftyadmin.cn/n/735072.html

相关文章

传统字符型验证安全现状及网易易盾验证码的优势

传统字符型验证安全现状 虽然是出于安全的考虑&#xff0c;但是越来越多的用户开始诟病这一反人类的设计发明&#xff0c;每天都要花部分时间 浪费在无趣的识别数字上&#xff0c;大大降低了一些网站的交互体验。同时&#xff0c;随着计算机自动识别技术的发展&#xff0c;简单…

数据库索引以及索引的实现(B+树介绍,和B树,区别)

数据库索引以及索引的实现(B树介绍&#xff0c;和B树&#xff0c;区别&#xff09; 索引 索引是提高数据库表访问速度的方法。 分为聚集索引和非聚集索引。 聚集索引&#xff1a;对正文内容按照一定规则排序的目录。 非聚集索引&#xff1a;目录按照一定的顺序排列&#xff0c;…

HTML 踩坑笔记: video 标签 autoplay 属性失效(Error: Uncaught (in promise) DOMException: play() failed)

HTML 踩坑笔记: video 标签 autoplay 属性失效(Error: Uncaught (in promise) DOMException: play() failed) 文章目录HTML 踩坑笔记: video 标签 autoplay 属性失效(Error: Uncaught (in promise) DOMException: play() failed)0. 项目背景1. 问题描述尝试: 主动调用 play 方法…

注册中心 Eureka 源码解析 —— 应用实例注册发现(六)之全量获取

摘要: 原创出处 http://www.iocoder.cn/Eureka/instance-registry-fetch-all/ 「芋道源码」欢迎转载&#xff0c;保留摘要&#xff0c;谢谢&#xff01; 本文主要基于 Eureka 1.8.X 版本 1. 概述2. Eureka-Client 发起全量获取 2.1 初始化全量获取2.2 定时获取2.3 刷新注册信息…

Flask 踩坑笔记: localhost:5000 无法访问/host,port 配置无效(Error: Failed to load resource: the server responded

Flask 踩坑笔记: localhost:5000 无法访问/host,port 配置无效(Error: Failed to load resource: the server responded with a status of 403 ()) 文章目录Flask 踩坑笔记: localhost:5000 无法访问/host,port 配置无效(Error: Failed to load resource: the server responded…

Yarn 升级: v3 都出了不要再用 yarn1 了

Yarn 升级: v3 都出了不要再用 yarn1 了&#xff01; 文章目录Yarn 升级: v3 都出了不要再用 yarn1 了&#xff01;Yarn 默认版本Yarn 升级公告开始升级&#xff01;第一步&#xff1a;初始化项目/现有项目升级第二步&#xff1a;安装依赖查看变化小结参考连接完整代码示例Yarn…

Linux SNMP 监控一些常用OID

Linux SNMP 监控一些常用OID linux服务器snmp常用oid http://www.haiyun.me/archives/linux-snmp-oid.html 收集整理一些Linux下snmp常用的OID&#xff0c;用做服务器监控很不错。服务器负载&#xff1a; 1231 minute Load: .1.3.6.1.4.1.2021.10.1.3.15 minute Load: .1.3.6.1…

细说 Nginx: 静态资源服务器基础 - server,location,root

细说 Nginx: 静态资源服务器基础 - server,location,root 文章目录细说 Nginx: 静态资源服务器基础 - server,location,rootNginx 概述 & 安装简要安装Nginx 配置文件结构实验1: 第一个路由实验2: 更多的服务器2.1 location 路径匹配实验3: 代理服务器实验小结参考连接完整…