React状态管理库Recoil浅入浅出

React状态管理库Recoil浅入浅出

一、引言

Recoil 的 slogan 十分简单:一个react状态管理库(A state management library for React)

Recoil 的产生源于 Facebook 内部一个可视化数据分析相关的应用,在使用 React 的实现的过程中,因为现有状态管理工具不能很好的满足应用的需求,因此催生出了 Recoil 。

这个应用带有复杂的交互,可以被总结为以下特点:

  • 大量需要共享状态的场景
  • 大量需要派生状态(基于某些状态计算出一个新的状态)的场景
  • 状态可以被持久化,进而通过被持久化的状态恢复当时场景

二、注意📢

但需要注意的是,此项目是facebook内部的实验性项目,截止2022年11月8日,此项目目前的最新的版本号是0.7.6,还没有出正式版本。

image-20221108093737186

包括其官方文档看着看着就会出来个 UNSTABLE 或者UNSAFE的标签

image-20221108093951738

如果你的项目,大型项目或者维护很持久的项目(ps: 3、5年以上的)个人不是很推荐用,小项目可以试水,完全没有问题。写法很舒服,函数式调用及其简易。

三、其他状态管理方案比较

当然共享状态还有其他方法:

  • React自身

  • Redux

  • Mobx

  • Context

3.1. 各个方案NPM Trends对比

image-20221108162825008

3.2. Stars 对比

image-20221108162931404

如果用这些方法的会出现什么问题或者说一些缺点不太舒服的地方?当然它们本身都很优秀,不然也不会那么大的下载量。

3.3. React自身

React本身解决数据共享是通过提升状态来解决的,这个就自然就会因为某个组件的状态改变而导致所有的子组件重新渲染,尽管我们可以使用memo来优化,但是唤醒的问题依然存在,对比前后的Props的操作依然无法避免。另一个问题是,一旦又有一个组件需要观察共享的数据,那又需要继续提升数据,又麻烦了

3.4. Redux

Redux 其大致工作流程,一般来说是这样的:

  1. 用户在页面上进行某些操作,通过 dispatch 发送一个 action
  2. Redux 接收到这个 action 后通过 reducer 函数获取到下一个状态。
  3. 将新状态更新进 storestore 更新后通知页面进行重新渲染。

从这个流程中不难看出,Redux 的核心就是一个 「发布-订阅」 模式。view 订阅了 store 的变化,一旦 store 状态发生修改就会通知所有的订阅者,view 接收到通知之后会进行重新渲染。

3.5. Mobx

Mobx 是 React 的另一种经过战火洗礼的状态管理方案,和 Redux 不同的地方是 Mobx 是一个响应式编程(Reactive Programming)库

Mobx 借助于装饰器的实现,使得代码更加简洁易懂。由于使用了可观察对象,所以 Mobx 可以做到直接修改状态,而不必像 Redux 一样编写繁琐的 actions 和 reducers。

这里的 action 不是必须的,但为了保证状态不会被随意修改,还是建议开启严格模式,只允许在 action 里面修改状态。

1
2
3
4
5
6
7
8
9
10
11
import { action, observable } from 'mobx';

class Store {
@observable count = 0;
@action increment() {
this.count++;
}
@action decrement() {
this.count--;
}
}

image-20221013153014972

3.6. Redux 和 Mobx总结

  • 一个Action会唤醒所有的订阅数据的组件,即使他们订阅的数据并没有发生变化,而且只能在数据变化后,通过浅比较(或者深比较)的方式对比前后的数据是否一致,来阻止无效渲染。(Mobx基本同理)
  • 只能通过状态提升至公共祖先来共享状态,但可能导致一颗巨大的树重新渲染。
  • 上下文(context)只能存储一个值,而不能存储一组不确定的值,且每个值都有自己的使用者(consumers)。
  • 这两种方式都很难将组件树的叶子节点(使用状态的地方)与组件树的顶层(状态必须存在的地方)进行代码分拆。

3.7. Context

如果仅仅在依赖数据变化时候才更新的场景,使用Context可以完美的解决,当Context的数据变化时,只有监听了相关Context的组件才会重新渲染。但Context的问题是过于动态性的场景可能有些许问题,比如用户通过点击按钮添加内部的某个小组件,这样的话 对应的Context也需要动态的插入到顶层组件,方便共享数据给其他组件,但是由于React的diff策略,如果在顶层组件动态插入Context或任何组件,就会导致子组件树不断被 销毁重建,损耗性能。

四、Recoil介绍

Recoil的设计思想就是我们把状态拆分成一个个的Atom(原子),再由Selector派生出更多的状态,最后React的组件树订阅自己需要的状态,当有原子状态更新,只有改变的原子及其下游节点有订阅他们的组件才会更新。

Copy:也就是说,Recoil其实构建了一个有向无环图,这个图和React组件树正交,他的状态和React组件树是完全解耦的。

Recoil独立于React,单独构建出一套自己的状态树,这个状态树平行于组件树而存在,状态树由Atom和Selector构成。

img

状态树的基本单位是Atom(原子),一个Atom表示一份可变,可被订阅的状态,当Atom代表的状态改变时,只会重新渲染订阅了这个Atom的组件,而不会影响其他组件。

五、Recoil使用

Recoil是专门为React设计的状态管理库,他的API满满的“react风格”。 Recoil 只支持hooks API,在使用上来说可以说十分简洁。

5.1 RecoilRoot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
import ReactDOM from 'react-dom/client';
import { RecoilRoot } from 'recoil';
import App from './App';
import './index.css';

const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<RecoilRoot>
<React.Suspense fallback={<div>Loading...</div>}>
<App />
</React.Suspense>
</RecoilRoot>
</React.StrictMode>
);
useRecoilState

当组件同时需要读写状态时,推荐使用该 hook。

const [comState, setComState] = useRecoilState(commonState);

useRecoilValue

当一个组件需要在不写入 state 的情况下读取 state 时,推荐使用该 hook。

const comState1 = useRecoilValue(commonState);

useSetRecoilState

​ 返回一个 setter 函数,用来更新可写 Recoil state 的值,状态变化不会导致组件重新渲染。

​ 当一个组件需要写入而不需要读取 state 时,推荐使用此 hook。

​ 如果组件使用了 useRecoilState()来获取 setter 函数,那么同时它也会订阅更新,并在 atom 或 selector 更新时重新渲染。使用 useSetRecoilState() 允许组件在值发生改变时而不用给组件订阅重新渲染的情况下设置值。

const setComState = useSetRecoilState(commonState);

5.2 Atom

一个 atom 代表一个状态。Atom 可在任意组件中进行读写。读取 atom 值的组件隐式订阅了该 atom,因此任何 atom 的更新都将导致订阅该 atom 的组件重新渲染:

1
2
3
4
5
6
import {atom} from "recoil";

const textState = atom({
key: 'textState', // // 全局下保持唯一性
default: '', // 初始值
});

在需要向 atom 读取或写入的组件中,应该使用 useRecoilState(),如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import {useRecoilState, useRecoilValue, useSetRecoilState} from "recoil";
import {textState, charCountState} from "../store/";

export default function HomePage(props) {
return (
<div>
<h3>HomePage</h3>
<TextInput />
<CharacterCount />
<SetButton />
</div>
);
}


function TextInput() {
const [text, setText] = useRecoilState(textState);

const onChange = event => {
setText(event.target.value);
};

return (
<div>
<input type="text" value={text} onChange={onChange} />
<p></p> Echo: {text}
</div>
);
}

5.3 Selector

selector 代表一个派生状态,派生状态是状态的转换。你可以将派生状态视为将状态传递给以某种方式修改给定状态的纯函数的输出:

1
2
3
4
5
6
7
8
9
10
import {selector} from "recoil";

const charCountState = selector({
key: 'charCountState', // 全局下保持唯一性
get: ({get}) => {
const text = get(textState);

return text.length;
},
});

这里想要获取textState的长度,因此只需读取值就可以 了,可以使用 useRecoilValue() 这一 hook:

1
2
3
4
5
6
7
import {useRecoilValue} from "recoil";

function CharacterCount() {
const count = useRecoilValue(charCountState);

return <p>Character Count: {count}</p>;
}

六、Example

ToList应用

七、总结

虽然React相关的状态管理库很多,但是Recoil的一些思想还是非常先进的,社区对Recoil关注度也很高,目前 star 18k。但还是跟上述的注意事项一样,还不是稳定版本,npm下载量并不是特别高,大项目生产环境使用不推荐。

附1. 简易版Recoil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import { useState, useEffect, useCallback } from 'react';

// An interface with the disconnect method. This could just be a function
// but I think having it as an object is more readable.
interface Disconnect {
disconnect: () => void;
}

// `Stateful` is the base class that manages states and subscriptions.
// Both Atom and Selector are derived from it.
export class Stateful<T> {
// This is a set of unique callbacks. The callbacks are listeners
// that have subscribed
private listeners = new Set<(value: T) => void>();

// The value property is protected because it needs to be manually
// assigned in the constructor (because of inheritance quirks)
constructor(protected value: T) { }

// Simple method for returning the state. This could return a deep
// copy if you wanted to be extra cautious.
snapshot(): T {
return this.value;
}

// The emit method is what updates all the listeners with the new state
private emit() {
for (const listener of Array.from(this.listeners)) {
listener(this.snapshot());
}
}

// The update method is the canonical way to set state. It uses object
// equality to prevent unnecessary renders. A deep comparison could be
// performed for complex objects that are often re-created but are the
// same.
protected update(value: T) {
if (this.value !== value) {
this.value = value;
// After updating the value, let all the listeners know there's a
// new state.
this.emit();
}
}

// The subscribe method lets consumers listen for state updates. Calling
// the `disconnect` method will stop the callback from being called in
// the future.
subscribe(callback: (value: T) => void): Disconnect {
this.listeners.add(callback);
return {
disconnect: () => {
this.listeners.delete(callback);
},
};
}
}

// The atom is a thin wrapper around the `Stateful` base class. It has a
// single method for updating the state.
//
// Note: `useState` allows you to pass a reducer function, you could add support
// for this if you wanted.
export class Atom<T> extends Stateful<T> {
public setState(value: T) {
super.update(value);
}
}

// The Recoil selector function is a bit gnarley. Essentially the "get" function
// is the way that selectors can subscribe to other selectors and atoms.
type SelectorGenerator<T> = (context: { get: <V>(dep: Stateful<V>) => V }) => T;

// The selector class. It extends `Stateful` so that it can be used as a value like
// atoms.
export class Selector<T> extends Stateful<T> {
// Keep track of all the registered dependencies. We want to make sure we only
// re-render once when they change.
private registeredDeps = new Set<Stateful<any>>();

// When the get function is called, it allows consumers to subscribe to state
// changes. This method subscribes to the dependency if it hasn't been already,
// then returns it's value.
private addDep<V>(dep: Stateful<V>): V {
if (!this.registeredDeps.has(dep)) {
dep.subscribe(() => this.updateSelector());
this.registeredDeps.add(dep);
}

return dep.snapshot();
}

// A helper method for running the internal generator method, updating dependencies,
// returning the computed state and updating all listeners.
private updateSelector() {
this.update(this.generate({ get: dep => this.addDep(dep) }));
}

constructor(private readonly generate: SelectorGenerator<T>) {
// This needs to be undefined initially because of Typescript's inheritance rules
// It's effectively "initialised memory"
super(undefined as any);
this.value = generate({ get: dep => this.addDep(dep) });
}
}

// A helper function for creating a new Atom
// The `key` member is currently unused. I just kept it around to maintain a similar
// API to Recoil.
export function atom<V>(value: { key: string; default: V }): Atom<V> {
return new Atom(value.default);
}

// A helper method for creating a new Selector
// Likewise the `key` method is just for looking like Recoil.
export function selector<V>(value: {
key: string;
get: SelectorGenerator<V>;
}): Selector<V> {
return new Selector(value.get);
}

// This hook will re-render whenever the supplied `Stateful` value changes.
// It can be used with `Selector`s or `Atom`s.
export function useCoiledValue<T>(value: Stateful<T>): T {
const [, updateState] = useState({});

useEffect(() => {
const { disconnect } = value.subscribe(() => updateState({}));
return () => disconnect();
}, [value]);

return value.snapshot();
}

// Similar to the above method, but it also lets you set state.
export function useCoiledState<T>(atom: Atom<T>): [T, (value: T) => void] {
const value = useCoiledValue(atom);
return [value, useCallback(value => atom.setState(value), [atom])];
}

参考

  1. Recoil官网
  2. React 下一代状态管理库 Recoil by 字节
  3. 👍🏻React下一代状态管理库——recoil by 头条前端
  4. Recoil 用法及原理浅析
感谢你的打赏哦!