我想它或许是拥抱hooks的最好用的状态管理工具吧
Recoil目前是Facebook一个实验性质的用于React的状态管理库。
官网地址: https://recoiljs.org
特性
- Recoil沿用React的设计思路和工作方式,给你的应用提供更快速和可伸缩的状态管理方案
- 派生状态(这里我觉得叫计算属性可能更方便理解)和异步查询都以纯函数和高效的订阅方式提供
- 在不影响代码分割的情况下,通过监听应用程序中的所有状态更改来实现持久化、路由、时间旅行调试、撤消。
动机
使用React内置的状态管理会比使用外部的库更好,但React本身存在一些限制:
- 组件需要共享的状态需要放到共同的祖先组件之中,但这可能会引起一些不必要的重新渲染
- Context只能存储单一的value值,而不是一组拥有各自独立的消费者的值的集合
- 以上的这些会导致代码分割变得更难
Recoil管理全局数据流管理,采用了一种松散的原子状态管理的设计模式,所有的原子状态(Recoil里叫做atoms)的改变时通过纯函数(Recoil里叫做selectors)将改变下发到对应的组件里。通过这些方法:
- Recoil提供了一个和组件局部状态调用一致的get/set API
- 我们可以兼容Concurrent Mode以及未来React提供的各种特性
- 状态定义是增量和分布式的,这让代码分割变得可行
- 不需要修改组件中调用,就可以把状态更改为派生状态
- 派生状态改成异步或者同步,都不会影响到组件里调用的写法
- We can treat navigation as a first-class concept, even encoding state transitions in links.
- 更容易把状态持久化转换成后端兼容的模式
核心概念
Atoms
Atoms是状态的单元,它可被更新,也可被订阅:当atom更新时,每个监听atom的组件都会获得一个新的值,并触发组件的重新渲染。它也可以在运行时创建。atoms可以用来替代部分的React组件局部状态。如果同一个atom在多个组件中使用,那这些组件则会共享这个状态。
import {atom} from 'recoil'
const fontSizeState = atom({
key: 'fontSizeState',
default: 14,
});
Atoms需要一个全局唯一的key,以及一个默认值default。通过我们提供的api:useRecoilState 就可以对这个状态进行读/写:
import {useRecoilState} from 'recoil'
function FontButton() {
// 就和React的useState一样,减少学习的心智负担
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
return (
<button onClick={() => setFontSize((size) => size + 1)} style={{fontSize}}>
Click to Enlarge
</button>
);
}
Selectors
Selectors是一个纯函数(划重点,不能有副作用,因为会多次执行),输入参数接收atoms或者其他Selectors。当Selectors依赖的atoms/Selectors改变时,select函数会进行重新评估/计算。组件监听到Selectors改变之后,就会执行重新渲染。
Selectors通常用于基于已有状态的计算出派生状态,这让我们可以减少一些冗余状态的使用。在atoms中我们存储的是状态的最小集合,而其他的计算属性/派生状态都可以是基于这个最小集合的高效运行的函数。
Selectors通常使用selector函数声明:
const fontSizeLabelState = selector({
key: 'fontSizeLabelState',
get: ({get}) => {
// 这里的fontSizeState是前面定义的atoms
// 这里的get可以依赖fontSizeState改变而重新计算
const fontSize = get(fontSizeState);
const unit = 'px';
return `${fontSize}${unit}`;
},
});
Selectors一般是只读的(可写的Selectors可进一步参考相关API文档 ,有过vue、ko使用经验的小伙伴应该对这些理解起来会更容易一些),我们可以使用useRecoilValue()
获取Selectors的值
import {useRecoilState, useRecoilValue} from 'recoil'
function FontButton() {
const [fontSize, setFontSize] = useRecoilState(fontSizeState);
const fontSizeLabel = useRecoilValue(fontSizeLabelState);
return (
<>
<div>Current font size: ${fontSizeLabel}</div>
<button onClick={() => setFontSize(fontSize + 1)} style={{fontSize}}>
Click to Enlarge
</button>
</>
);
}
使用指导
只使用值
如果组件中只需要使用值,上面有Selectors相关章节有提到过useRecoilValue,它除了可以接收Selectors之外也可以接收atoms。所以在一些只用于纯展示值的组件中,完全可以只引入值,不引入set方法:
const todoListState = atom({
key: 'todoListState',
default: [],
});
function TodoList() {
// 只引入值
const todoList = useRecoilValue(todoListState);
return (
<>
{todoList.map((todoItem) => (
<TodoItem key={todoItem.id} item={todoItem} />
))}
</>
);
}
只使用set方法
同样,对于一些交互类的组件,我们可能只需要使用更改状态的set方法,那么我们引入useSetRecoilState就可以里,如下面的例子:
import {atom, useSetRecoilState} from 'recoil';
const namesState = atom({
key: 'namesState',
default: ['Ella', 'Chris', 'Paul'],
});
function FormContent({setNamesState}) {
const [name, setName] = useState('');
return (
<>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
<button onClick={() => setNamesState(names => [...names, name])}>Add Name</button>
</>
)}
// This component will be rendered once when mounting
function Form() {
const setNamesState = useSetRecoilState(namesState);
return <FormContent setNamesState={setNamesState} />;
}
同/异步数据
Recoil最大的好处之一在于,处理同步和异步数据时,组件内部引用的写法始终是一致且简单的。
下面的例子使用同步的atoms和selectors来获取数据:
// 同步获取数据
const currentUserIDState = atom({
key: 'CurrentUserID',
default: 1,
});
const currentUserNameState = selector({
key: 'CurrentUserName',
get: ({get}) => {
return tableOfUsers[get(currentUserIDState)].name;
},
});
function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameState);
return <div>{userName}</div>;
}
function MyApp() {
return (
<RecoilRoot>
<CurrentUserInfo />
</RecoilRoot>
);
}
下面的例子是异步获取数据的例子:
const currentUserNameQuery = selector({
key: 'CurrentUserName',
// 通过async、await来获取异步数据
get: async ({get}) => {
const response = await myDBQuery({
userID: get(currentUserIDState),
});
return response.name;
},
});
function CurrentUserInfo() {
// 同样顺滑的写法,和useState一样
const userName = useRecoilValue(currentUserNameQuery);
return <div>{userName}</div>;
}
同时,Recoil还可以和Suspense一起使用:
function MyApp() {
return (
<RecoilRoot>
<React.Suspense fallback={<div>Loading...</div>}>
<CurrentUserInfo />
</React.Suspense>
</RecoilRoot>
);
}
// 或者不使用Suspense
function UserInfo({userID}) {
const userNameLoadable = useRecoilValueLoadable(currentUserNameQuery);
switch (userNameLoadable.state) {
case 'hasValue':
return <div>{userNameLoadable.contents}</div>;
case 'loading':
return <div>Loading...</div>;
case 'hasError':
throw userNameLoadable.contents;
}
}
并发请求
使用waitForAll和waitForNone来实现并发异步请求
详见 https://recoiljs.org/docs/guides/asynchronous-data-queries/#concurrent-requests
总结
recoil目前只支持hooks api的使用,主打高性能以及利用React内部的调度机制,可以不用像Redux那样编写冗长的代码,同时自身又能支持异步处理和缓存计算。可以说在使用hooks的开发场景下,和useState类似的api使用可以很好的减轻开发者学习的心智负担。当然react-redux 7.1版本也推出来hooks的api供使用,两者的对比,我会考虑在后续的文章中再介绍一下。另外再次提示大家,目前Recoil还处于试验阶段,个别api的设计可能还不稳定,有待进一步的跟进。