React状态管理库Recoil教程(学习使用记录)

一、前言

闲暇期间自学了一点点React,尝试用来构建下简单的网页,学习内容的话应该是算浅尝辄止的程度,实际运用起来感觉是经常遇到一些奇怪的问题,特别是使用Hooks来管理状态时,经常遇到状态该更新却未更新之类的情况或者页面不断re-render很快超出最大render限度。

虽然就个人提示来说,更应该认真研究,填上这个坑,提高个人能力,但由于本来的工作(机械行业)过重,又经常加班,深入研究一是时间不多,二是半路出家,还是野生的,有时想学习却不知道怎样或去哪里学习。

这样一来,个人就觉得先学习一个现成的状态管理库,撸下实操代码练习一下。首先是看了下React界最负盛名的Redux,了解了下(看了一点教程),感觉比较复杂,特别是随便一个网页多少都会有异步过程,Redux异步状态更新需要由中间件介入,然后action、reducer等感觉定义起来也比较麻烦。当然也试过直接用context,个人水平太差不适应。最后选了这个Facebook出的实验性质的库Recoil,这个库从理解到使用来说,都更加简单,比较适合我这种水平的人尝试一下(自己乱用也是,实际也不知道对不对),这里记录下,因为记忆力实在不好,后面使用时还是需要照着搞。文章举例基本是使用了我自己尝试搞的一个zxj-demo中的代码,会有些瑕疵。

二、简介

Recoil在 React tree 上创建了另一个正交的 tree,把组件需要管理的 state 抽出来。每个 component 都有对应单独的一部分 state,当state数据更新的时候对应的组件也会更新。Recoil 把 这每一片的数据称为 Atom,Atom 是可订阅可变的 state 单元。配合 useRecoilState等API 可以使用这些 Atom。

Recoil是原子化状态管理,粒度较小(特别是跟整个状态在一起的Redux比),所以使用起来感觉更简单一些。

核心概念

Atom:Atom 是最小状态单元。它们可以被订阅和更新:当它更新时,所有订阅它的组件都会使用新数据重绘;它可以在运行时创建;它也可以在局部状态使用;同一个 Atom 可以被多个组件使用与共享。

Selector:selector 是一个入参为 Atom 或者其他 Selector 的纯函数。当它的上游 Atom 或者 Selector 更新时,它会进行重新计算。Selector 可以像 Atom 一样被组件订阅,当它更新时,订阅它的组件将会重新渲染。Selector 通常用于计算一些基于原始状态的派生数据。因为不需要使用 reducer 来保证数据的一致性和有效性,所以可以避免冗余数据。使用 Atom 保存原始状态,其他数据都是在其基础上计算得来的。因为 Selector 会追踪使用它们的组件以及它们依赖的数据状态,所以函数式编程会比较高效.

三、安装

使用如下命令安装最新版本的Recoil:

# npm:
npm install recoil --save

# yarn:
yarn add recoil

四、基本使用方法

  • 使用RecoilRoot组件包裹要进行状态管理(共享)组件的父组件,当然完全可以直接包裹最顶层的App组件。
  • 在一个(或几个)文件(比如就命名为store.js之类,本文以此文件名举例)中定义好Atom、Selector等,并export,以便需要进行状态管理的组件进行使用。
  • 异步操作使用selector/selectorFamily。
  • atom、selector、atomFamily、selectorFamily这些API是类似纯函数概念,同输入会得到同输出(比如selectorFamily如果相同输入的话后续取值时不会进行计算,直接会采用缓存的值,需要异步取值且值可能不同时可能要考虑增加其他输入参数,以保证输入不同,触发selectorFamily的重新计算)。

五、API及使用

1、RecoilRoot

对于使用 Recoil 的组件,需要将 RecoilRoot 放置在组件树上的任一父节点处。最好将其放在根组件中:

# 示例(包裹根组件):
import { RecoilRoot } from 'recoil';
export default const App = () => {
  return (
    <RecoilRoot>
       <其他内容/>
    </RecoilRoot>
);

}

RecoilRoot提供了上下文,此组件必须是所有使用 Recoil hook 的根组件。其中多个根组件可以并存,atom 在不同根组件的内部有着不同的值。如果它们互相嵌套了,则最内部的根组件会完全覆盖其他所有的外部根组件。RecoilRoot组件可以接收一个名为initializeState的props,形式为({set, setUnvalidatedAtomValues}) => void,可以用于设置atom的初始值,就我使用的情形一般是用不到。

2、Atom相关

一个 atom 代表一个状态。Atom 可在任意组件中进行读写。读取 atom 值的组件隐式订阅了该 atom,任何 atom 的更新都将导致订阅该 atom 的组件重新渲染,使用atom进行定义,其中key应是一个唯一的字符串,default为该atom的初始状态(可以为一个值、其他同类的atom/selector、一个可以获取值的同步函数)。

# store.js:
import { atom } from 'recoil';
export const userinfoState = atom({
  key: 'userinfoState',
  default: null,
});

使用atom:

import { useRecoilValue,useRecoilState,useSetRecoilState,useRestRecoilState,useRecoilCallback } from 'recoil';
import { userinfoState } from 'recoil';

# 仅读不写(会订阅更新):
const userinfo = useRecoilValue(userinfoState);
# 既读又写(会订阅更新,可以更新atom状态,类似于useState):
const [userinofo,setUserinfoState] = useRecoilState(userinfoState);
# 仅写不读(不订阅更新,仅用于修改atom):
const setUserinfo = useSetRecoilState(userinfoState);

# 读但不订阅,使用useRecoilCallback 通过回调方式定义要读取的数据,这个数据变化也不会导致当前组件重渲染:
const getuserinfo = useRecoilCallback(async ({ getPromise }) => {
    const userinfo = await getPromise(userinfoState); 
    console.log(useinfo);
  }

3、Selector相关

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

# store.js:
import { atom,selector } from 'recoil';
#待回答问题列表:
export const replylistState = atom({
  key: 'replylistState',
  default: null,
});

# 待回答问题数量:
export const replyjobcountValue =  selector({
  key: 'replyjobcountValue',
  get: ({get}) => {
    const replylist = get(replylistState);
    if (replylist) {
      return replylist.filter(item => item.status === '提交').length;  
    }
    return 0;
  }
});

定义selector示例如上,key和get属性是必需的,还有一个set属性可选({get,set}=>{}),一般用来更新上游atom或selector。

这里的set、get入参是用于设置(赋值)和获取(取值)atom、selector的值的方法,用法:取值=get(atom/selector),赋值=set(atom/selector,新值)。

如果没有set属性,selector一般只能取值,即只能使用useRecoilValue,如果有set属性,则它可以像atom一样使用useRecoilValue/useRecoilState/useSetRecoilState进行取值、赋值。

4、atomFamily

atomFamily使用方法同atom基本相同,不同的是它的default属性可以接收参数。从字面上可以理解atomFamily就是一系列atom的集合,它们拥有相同的key,但内部会根据不同的参数映射到不同的值。

想根据参数对应不同的atom值则必须使用atomFamily,但应注意,相同的参数获得的值必须是相同的,也就是类似于一个纯函数,相同输入必得到相同输出,官方文档是说不应在atomFamily中进行异步操作,异步操作应使用selectorFamily。由于我并没有使用该api,这里记录下官网的例子:

# 官网示例1:
const myAtomFamily = atomFamily({
  key: ‘MyAtom’,
  default: param => defaultBasedOnParam(param),
});
# 官网示例2:
const myAtomFamily = atomFamily({
  key: ‘MyAtom’,
  default: selectorFamily({
    key: 'MyAtom/Default',
    get: param => ({get}) => {
      return computeDefaultUsingParam(param);
    },
  }),
});

5、selectorFamliy

同atomFamliy类似,selectorFamliy是接收参数,对应不同参数的selector的集合。下面是我尝试研究时的一个示例:

import { selectorFamily } from 'recoil'
import { post } from './post'
import { showURL } from './url'

export const questionQuery = selectorFamily({
  key: 'questionQuery',
  get: option => async () => {
    const res = await post(
      showURL,
      {
        opt: 'all',
        ...option,
      }
      );
    if (res.error) {
      throw res.error;
    }
    // console.log(res);
    return res.data.data;
  }
});

该例子是异步获取问题列表,它接收一个option参数,是查询参数,对于相同option该selectorFamliy不会触发后续的查询,直接返回之前相同option的缓存值,所以实际传入的option我定义的是一个对象{ ...一些查询参数,一个可选的递增的值 },如果需要强制获取最新内容,就会加上这个递增的值,避免读取缓存而不触发计算。

6、useRecoilValueLoadable()useRecoilStateLoadable()

分别类似于useRecoilValue、useRecoilState,用于读取异步selector的值(useRecoilStateLoadable还可以进行selector赋值,一般也不常用),但它不是返回异常或Promise,它会返回一个具有以下接口的 Loadablet对象:

  • state:表示 selector 的状态。可选的值有 'hasValue''hasError''loading'
  • contents:此值代表 Loadable 的结果。如果状态为 hasValue,则值为实际结果;如果状态为 hasError,则会抛出一个错误对象;如果状态为 loading,则值为 Promise

利用这2个API可以很好的处理异步获取数据时VIEW的处理(这里使用了一个其他的replyUpdateState的atom来进行必要时强制获取最新数据):

# 一个异步获取问题列表并显示的组件src/views/AsynAsked.js:
import { questionQuery } from '../utils/store'
import { useRecoilValueLoadable ,useRecoilState} from 'recoil';
import Loading from '../component/Loading'
import SummaryTable  from '../component/UncontrolledSummaryTable'
import { replyUpdateState } from '../utils/store'

const AsynAskedShow = (props) => {
  const options = { opt: 'asked', };
  const [shouldUpdate,setShouldUpdate ]= useRecoilState(replyUpdateState);
  const questions =  useRecoilValueLoadable(questionQuery({shouldUpdate,...options,}));
  
  switch (questions.state) {
    case 'hasValue':
      return (
        <div>
          <SummaryTable rows = {questions.contents} />          
        </div> 
      );
    case 'loading':
      return <Loading/>
    
    case 'hasError':
        throw questions.contents
}
}

export default AsynAskedShow;

7、其他

还有一些其他API由于没有使用就没有记录了,比如判断是否atom/selector的isRecoilValue等,具体使用时再查下官方文档

--------------------------------

除非注明,否则均为清风揽月阁原创文章,转载应以链接形式标明本文链接

本文链接:https://www.iimm.ink/327.html

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

回到顶部