React状态管理:Recoil指南

作者:renzp94
时间:2021-08-12 13:57:20

RecoilRoot

提供上下文。此组件必须是所有使用Recoil hook组件的根组件

  • initializeState:可使用MutableSnapshot初始化atom状态,可选函数(({set,setUnvalidatedAtomValues}) => void)
  • override:是否重新创建新的Recoil作用域,默认为true。一般用于多个<RecoilRoot>嵌套,当为true时会重新创建一个Recoil作用域,新的作用域会覆盖上级<RecoilRoot>中同名。为false则只渲染子组件,没有其他作用

通常情况下,atomselector需要配合以下hook使用: useRecoilState(读写),useRecoilValue(只读),useSetRecoilState(只写),useResetRecoilState(重置),现在只需要了解,这些hook后续会讲解

atom()

一个atom表示Recoilstate,接收一个对象,返回一个可写的RecoilState对象.接收对象属性如下:

  • key:标识当前atom的唯一key值,请记住:key需要保持全局唯一
  • default:默认值,可是任意类型的值,也可以是RecoilState(atom或selector),也可以是一个异步函数
  • dangerouslyAllowMutability:设置允许atom中的对象可变,对象变化不代表status的变化

Recoil管理atomstate的变化,并通知订阅该atom的组件何时渲染

import React from 'react';
import { atom, RecoilRoot, useRecoilState, useRecoilValue } from 'recoil';

const countState = atom({
    key: 'countState',
    default: 0
});

const Count = () => {
    const count = useRecoilValue(countState);
    return <span>{count}</span>;
};

const CountButton = (props: { type: string }) => {
    const [count, setCount] = useRecoilState(countState);
    return (
        <button onClick={() => setCount(count + (props.type === 'add' ? 1 : -1))}>
            {props.type === 'add' ? '+' : '-'}
        </button>
    );
};

const App = () => {
    return (
        <RecoilRoot>
            <CountButton type="dec" />
            <Count />
            <CountButton type="add" />
        </RecoilRoot>
    );
};

export default App;

如果需要在不订阅组件的情况下读取atom的值,可使用useRecoilCallback.如果需要基于上一个值更新的话,可以向set函数传入一个函数,函数的参数就是最新的值,返回一个需要设置的值.
default传入是一个函数时,则在使用hook返回的值也是一个函数.

import React from 'react';
import { atom, RecoilRoot, useRecoilValue } from 'recoil';

const msgState = atom({
    key: 'msg',
    default: () => 'Hello'
});

const Msg = () => {
    const msg = useRecoilValue(msgState);
    return <div>{msg()}</div>;
};

const App = () => {
    return (
        <RecoilRoot>
            <Msg />
        </RecoilRoot>
    );
};

export default App;

切记default不能传入一个异步函数,如果需要异步获取数据,可以使用selector包装一下,但一般如果需要异步请求的话,建议放在selector

import React, { Suspense } from 'react';
import { atom, RecoilRoot, selector, useRecoilValue } from 'recoil';

const msgState = atom({
    key: 'msg',
    default: selector({
        key: 'msgSelector',
        get: async () => {
            const data = await new Promise((resolve) => {
                setTimeout(() => resolve('Hello'), 2000);
            });

            return data;
        }
    })
});

const Msg = () => {
    const msg = useRecoilValue(msgState);
    return <div>{msg}</div>;
};

const App = () => {
    return (
        <RecoilRoot>
            <Suspense fallback={<div>loading...</div>}>
                <Msg />
            </Suspense>
        </RecoilRoot>
    );
};

export default App;

selector()

selector是一个纯函数,是派生状态,可基于atomselector返回处理过的状态,类似Vue中的compoted。接收一个对象,对象属性如下:

  • get:一个处理state的函数,返回处理过的值,也可返回一个异步的Promise或相同类型的atomselector。函数的参数是一个对象:
    • get:用来获取其他的atomselector值的函数,所有传入此函数的atomselector会被隐式的添加到此selector的依赖列表中,当依赖发生改变时,则会重新计算
    • getCallback:用于创建Recoil-aware回调的函数
  • set:若设置了该属性,则selector会返回一个可写的state,当改变selector则会调用此函数。此函数接收两个参数,第一个参数是一个对象,第二个参数是一个新值,第一个参数对象属性如下:
    • get:用来获取其他的atomselector值的函数
    • set:用来设置Recoil状态的函数,第一个参数是Recoilstate,第二个参数是新的值,新值可以是一个更新函数或一个DefaultVaue类型的对象
  • dangerouslyAllowMutability:设置允许atom中的对象可变,对象变化不代表status的变化
import React from 'react';
import { atom, RecoilRoot, selector, useRecoilState, useRecoilValue } from 'recoil';

const firstNameState = atom({
    key: 'firstName',
    default: 'Code'
});

const lastNameState = atom({
    key: 'lastName',
    default: 'Book'
});

const fullNameSelector = selector<string>({
    key: 'fullName',
    get: ({ get }) => `${get(firstNameState)}-${get(lastNameState) ?? ''}`,
    set: ({ set }, value) => {
        const [firstName, lastName] = value.split('-');
        set(firstNameState, firstName);
        set(lastNameState, lastName);
    }
});

const UpperFullNameSelector = selector<string>({
    key: 'upperFullName',
    get: ({ get }) => get(fullNameSelector).toUpperCase()
});

const Name = () => {
    const [firstName, setFirstName] = useRecoilState(firstNameState);
    const [lastName, setLastName] = useRecoilState(lastNameState);
    const [fullName, setFullName] = useRecoilState(fullNameSelector);
    const upperFullName = useRecoilValue(UpperFullNameSelector);

    return (
        <>
            <div>
                <span>firstName:</span>
                <input type="text" value={firstName} onChange={(e) => setFirstName(e.target.value)} />
            </div>
            <div>
                <span>lastName:</span>
                <input type="text" value={lastName} onChange={(e) => setLastName(e.target.value)} />
            </div>
            <div>
                <span>fullName:</span>
                <input type="text" value={fullName} onChange={(e) => setFullName(e.target.value)} />
            </div>
            <div>{upperFullName}</div>
        </>
    );
};

const App = () => {
    return (
        <RecoilRoot>
            <Name />
        </RecoilRoot>
    );
};

export default App;

selector还支持异步,由于React的渲染函数是同步的,所以需要Suspense边界包裹组件,需要捕获错误,可用ErrorBoundary包裹,如果需要查询时传入参数,可使用selectorFamily

import React, { Suspense } from 'react';
import { atom, RecoilRoot, selector, selectorFamily, useRecoilValue } from 'recoil';

// const userIdState = atom({
//     key:'userId',
//     default: 1
// })

// const userInfoSelector = selector({
//     key:'userInfo',
//     get: async ({get}) => {
//         const data = await new Promise(resolve => {
//             setTimeout(()=> resolve({id:get(userIdState),name:'用户001'}),2000)
//         })

//         return data
//     }
// })

const userInfoSelector = selectorFamily({
    key: 'userInfo',
    get: (id) => async () => {
        const data = await new Promise((resolve) => {
            setTimeout(() => resolve({ id, name: '用户001' }), 2000);
        });

        return data;
    }
});

const AsyncSelector = () => {
    const userInfo = useRecoilValue(userInfoSelector(1));

    return (
        <>
            <div>用户id:{userInfo.id}</div>
            <div>用户名:{userInfo.name}</div>
        </>
    );
};

const App = () => {
    return (
        <RecoilRoot>
            <Suspense fallback={<div>loading...</div>}>
                <AsyncSelector />
            </Suspense>
        </RecoilRoot>
    );
};

export default App;

如果需要并行请求数据可以使用waitForAll一次并行请求多条接口,也可以通过waiForOne实现增量请求更新。 一般数据会在渲染之前请求获取,在Recoil中可以使用useRecoilCallback

useRecoilState()

Recoil state读写,返回一个数组,数组第一个元素是state当前值,第二个元素是设置state值的函数。

useRecoilValue()

读取Recoil state的值,返回state的当前值。

useSetRecoilState()

设置Recoil state

useResetRecoilState()

重置Recoil state

useRecoilStateLoadable()

读写异步selector的值。返回一个数组,数组第一个元素是Loadable,第二个元素是设置的函数。使用此hooks可以不使用Suspense来使用异步selector

Loadable属性:

  • stateselector所处状态
  • hasValue:成功
  • loading:请求中
  • hasError:失败
  • contents:当前表示的值,hasValue状态时表示实际值,loading时表示PromisehasError时表示Error对象

useRecoilValueLoadable()

仅获取异步selector的值。返回的是一个Loadable,同useRecoilStateLoadable

import React, { Suspense } from 'react';
import { RecoilRoot, selectorFamily, useRecoilValue, useRecoilValueLoadable } from 'recoil';

const userInfoSelector = selectorFamily({
    key: 'userInfo',
    get: (id) => async () => {
        const data = await new Promise((resolve) => {
            setTimeout(() => resolve({ id, name: '用户001' }), 2000);
        });

        return data;
    }
});

const AsyncSelector = () => {
    const userInfo = useRecoilValueLoadable(userInfoSelector(1));

    switch (userInfo.state) {
        case 'hasValue':
            return (
                <>
                    <div>用户id:{userInfo.contents.id}</div>
                    <div>用户名:{userInfo.contents.name}</div>
                </>
            );
            break;
        case 'loading':
            return <div>loading...</div>;
        case 'hasError':
            return <div>error</div>;
    }
};

const App = () => {
    return (
        <RecoilRoot>
            <AsyncSelector />
        </RecoilRoot>
    );
};

export default App;

isRecoilValue()

是否为atomselector

atomFamily()

返回一个返回可写的RecoilState atom函数,用于表示一个atom的集合。当你需要给atom传参数时,可以使用此函数。

import React from 'react';
import { atomFamily, RecoilRoot, useRecoilValue } from 'recoil';

const msgState = atomFamily({
    key: 'msg',
    default: (msg: string) => msg.toUpperCase()
});

const Msg = () => {
    const msg = useRecoilValue(msgState('hello'));
    return <div>{msg}</div>;
};

const App = () => {
    return (
        <RecoilRoot>
            <Msg />
        </RecoilRoot>
    );
};

export default App;

selectorFamily()

返回一个函数,该函数返回一个只读的RecoilValueReadOnly或者可写的RecoilState selector。当你需要给selector传参数时,可以使用此函数

constSelector()

一个永远提供常量值的selector。(不太清楚有什么用)

errorSelector()

一个总是抛出已有错误的selector。(不太清楚有什么用)

noWait()

select helper方法,返回值代表所提供的atomselector当前状态的Loadable,与useRecoilValueLoadable()类似,但noWait是一个selector不是钩子函数

import React from 'react';
import { noWait, RecoilRoot, selector, useRecoilValue } from 'recoil';

const msgSelector = selector({
    key: 'msgSelector',
    get: async () => {
        const data = await new Promise((resolve) => {
            setTimeout(() => resolve('Hello'), 2000);
        });

        return data;
    }
});

const Msg = () => {
    const msgLoadable = useRecoilValue(noWait(msgSelector));

    return {
        hasValue: <div>{msgLoadable.contents}</div>,
        loading: <div>loading...</div>,
        hasError: <div>erro:{msgLoadable.contents}</div>
    }[msgLoadable.state];
};

const App = () => {
    return (
        <RecoilRoot>
            <Msg />
        </RecoilRoot>
    );
};

export default App;

waitFroAll()

并发计算多个异步依赖项的并发helper方法,即:可用于并发请求数据。接收一个数组或者对象。

import React from 'react';
import { RecoilRoot, selector, useRecoilValueLoadable, waitForAll } from 'recoil';

const userSelector = selector({
    key: 'userSelector',
    get: async () => {
        console.log('user start');
        const data = await new Promise((resolve) =>
            setTimeout(() => resolve({ id: 1, name: 'codebook' }), 2000)
        );
        console.log('user end');
        return data;
    }
});

const listSelector = selector({
    key: 'listSelector',
    get: async () => {
        console.log('list start');
        const data = await new Promise((resolve) => setTimeout(() => resolve([1, 2, 3, 4]), 2500));
        console.log('list end');
        return data;
    }
});

const Msg = () => {
    const { state, contents } = useRecoilValueLoadable(waitForAll([userSelector, listSelector]));

    switch (state) {
        case 'hasValue':
            const [user, list] = contents;
            return (
                <>
                    <div>用户名:{user.name}</div>
                    <div>列表数据:</div>
                    {list.map((item) => (
                        <div key={item}>{item}</div>
                    ))}
                </>
            );
        case 'loading':
            return <div>loading...</div>;
        case 'hasError':
            return <div>error: {contents}</div>;
    }
};

const App = () => {
    return (
        <RecoilRoot>
            <Msg />
        </RecoilRoot>
    );
};

export default App;

waitForAllSettled

并发计算多个异步依赖项的并发helper方法,即:可用于并发请求数据。接收一个数组或者对象。一直等待,直到有一个成功返回。

import React from 'react';
import { RecoilRoot, selector, useRecoilValueLoadable, waitForAllSettled } from 'recoil';

const userSelector = selector({
    key: 'userSelector',
    get: async () => {
        console.log('user start');
        const data = await new Promise((resolve) =>
            setTimeout(() => resolve({ id: 1, name: 'codebook' }), 2000)
        );
        console.log('user end');
        return data;
    }
});

const listSelector = selector({
    key: 'listSelector',
    get: async () => {
        try {
            console.log('list start');
            const data = await new Promise((resolve, reject) => setTimeout(reject, 3000));
            return data;
        } finally {
            console.log('list end');
        }
    }
});

const Msg = () => {
    const { state, contents } = useRecoilValueLoadable(
        waitForAllSettled([userSelector, listSelector])
    );

    switch (state) {
        case 'hasValue':
            const [{ contents: user }, { contents: list }] = contents; // => user: {id:1,name:'codebook} list: undefined
            return (
                <>
                    <div>用户名:{user.name}</div>
                    <div>列表数据:</div>
                    {list?.map((item) => (
                        <div key={item}>{item}</div>
                    ))}
                </>
            );
        case 'loading':
            return <div>loading...</div>;
        case 'hasError':
            return <div>error: {contents}</div>;
    }
};

const App = () => {
    return (
        <RecoilRoot>
            <Msg />
        </RecoilRoot>
    );
};

export default App;

waitForNone()

并发计算多个异步依赖项的并发helper方法,即:可用于并发请求数据。接收一个数组或者对象。与waitForAll类似,区别在于waitForNone会立即为每个依赖返回一个Loadable,而不是直接返回值

waitForAny()

并发计算多个异步依赖项的并发helper方法,即:可用于并发请求数据。接收一个数组或者对象。一直等待,直到有一个成功返回

useRecoilCallback

类似useCallback,这个回调可以访问Recoil状态的只读Snapshot,并且能够异步更新当前的Recoil状态。使用场景:

  • 异步读取Recoil状态,而无需订阅React组件在atomselector更新时重新渲染
  • 把昂贵的查询延迟到一个你不想在渲染时执行的异步操作
  • 在你想读取 或写入Recoil状态的地方执行副作用
  • 动态更新一个atomselector,可能并不知道在渲染时要更新哪个atomselector,所以不能使用useRecoilState()
  • Pre-fetching渲染前的数据

接收参数和useCallback一样,包括传入空数组只在初始化后执行一次,传入依赖项,当依赖项发生变化,重新执行。

回调函数的参数:

  • snapshotSnapshot提供一个只读的Recoil atom状态
  • gotoSnapshot:更新全局状态以匹配提供的Snapshot
  • set:设置atomselector的值
  • reset:重置atomselector的值