我想它或许是拥抱hooks的最好用的状态管理工具吧

Recoil目前是Facebook一个实验性质的用于React的状态管理库。
官网地址: https://recoiljs.org

特性

  1. Recoil沿用React的设计思路和工作方式,给你的应用提供更快速和可伸缩的状态管理方案
  2. 派生状态(这里我觉得叫计算属性可能更方便理解异步查询都以纯函数和高效的订阅方式提供
  3. 在不影响代码分割的情况下,通过监听应用程序中的所有状态更改来实现持久化、路由、时间旅行调试、撤消。

    动机

    使用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在多个组件中使用,那这些组件则会共享这个状态。

  1. import {atom} from 'recoil'
  2. const fontSizeState = atom({
  3. key: 'fontSizeState',
  4. default: 14,
  5. });

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的设计可能还不稳定,有待进一步的跟进。