Recoil的应用实践

563 阅读3分钟

简介

基本信息

  • FaceBook推出的新一代React状态管理工具
  • Recoil是专门为React而生的
    • 只能用在React体系(React,preact)中使用Recoil (不能在其他非React体系的框架中使用)
    • 不能在类组件中使用Recoil (Recoil中使用React Hooks实现)

设计理念

  • Redux
    • 集中式状态管理(单一数据流)
    • 学习成本比较高(store | state | action | reducer)
    • 非官方(Redux | Redux-thunk | Redux-saga | Redux-promise)
  • Recoil
    • 分散的状态管理(Atom | Selecter)
    • 上手快(Hooks语法)使用recoil里面的钩子
    • 官方出品(Facebook)

最佳实践

  • 安装
    • npm install recoil
  • 使用
      1. 初始状态(RecoilRoot)
      1. 定义状态 (atom | selector)
      1. 使用状态(Recoil Hooks)
import React from 'react';
import { RecoilRoot } from 'recoil';
import App from './App';

return (
  <RecoilRoot>
      <App />
  </RecoilRoot>
)

recoiljs.org/zh-hans/doc…

重要概念

  • atom (原子状态)
function atom<T>({
  // 在内部用于标识 atom 的唯一字符串
  key: string,
  // atom 的初始值,或一个 Promise,或另一个 atom,或一个用来表示相同类型的值的 selector
  default: T | Promise<T> | RecoilValue<T>,
	// 副作用: 比如状态更新时,还需要更新其他的逻辑可以在effets中操作
  effects?: $ReadOnlyArray<AtomEffect<T>>,
	// 取消 Immutable
  dangerouslyAllowMutability?: boolean,
}): RecoilState<T>

dangerousAllowMutability: 在某些情况下,我们可能希望允许存储在 atom 中的对象发生改变,而这些变化并不代表 status 的变更。使用这个选项可以覆盖开发模式下的 freezing 对象

  • atom是存储状态的最小单位
  • atomFamily - 允许传参
// atom.ts
export const cartState = atomFamily({
    key: 'blFDTUf/$$/cartState',
    default: (param) => param
});

// 调用
const numItemsInCart = await snapshot.getPromise(cartState(num));

www.recoiljs.cn/docs/api-re…

  • selector (衍生状态|计算状态)
function selector<T>({
  key: string,
	// 获取其他的原子状态或者衍生状态
  get: ({
    get: GetRecoilValue,
    getCallback: GetCallback,
  }) => T | Promise<T> | RecoilValue<T>,

  set?: (
    {
      get: GetRecoilValue,
      set: SetRecoilState,
      reset: ResetRecoilState,
    },
    newValue: T | DefaultValue,
  ) => void,

  dangerouslyAllowMutability?: boolean,
  // 指定缓存策略 不稳定
  // lru  当大小超过时,从缓存中收回最近最少使用的值
  // most-recent  仅保留最新值
  // keep-all  保留缓存中的所有条目
  cachePolicy_UNSTABLE?: CachePolicy,
})
  • selector是以其他状态(atom | selector)为参数的纯函数
  • selectorFamily - 允许传参
// atom.ts
// 文章详情
export const postDetailState = atomFamily({
    key: 'OobpCIC/$$/postDetailState',
    default: selectorFamily({
        key: 'bnIBICN/$$/postDetailState/Default',
        get:
            (postId) =>
            async ({ get }) => {
                const res: any = await ruyi.postsDetail$.get({
                    postId
                });
                if (res.code === 200) {
                    return res.data;
                }
                return {};
            }
    })
});

// 调用
postDetailState(query?.id)

recoiljs.org/docs/api-re…

Recoil Hooks

同步

  • 声明状态
    • const recoilState = atom | atomFamily | selector | selectorFamily | ...
  • 读和写
    • const [stateValue,setStateValue] = useRecoilState(recoilState)
    • 当组件同时需要读写状态时,推荐使用该 hook,在 React 组件中,使用将会订阅该组件
    • www.recoiljs.cn/docs/api-re…
    • const stateValue = useRecoilValue(recoilState)
    • 当一个组件需要在不写入 state 的情况下读取 state 时,推荐使用该 hook,在 React 组件中,使用本 hook 将会订阅该组件,并且在 state 更新时重新渲染该组件。
    • www.recoiljs.cn/docs/api-re…
    • const setStateValue = useSetRecoilState(recoilState)
    • 当一个组件需要写入而不需要读取 state 时,推荐使用此 hook。
    • 使用 useSetRecoilState() 允许组件在值发生改变时而不用给组件订阅重新渲染的情况下设置值。(有问题)
    • www.recoiljs.cn/docs/api-re…
  • 重置状态
    • const resetStateValue = useResetRecoilState(recoilState)
    • useResetRecoilState(state) 返回一个函数,用来把给定 state 重置为其初始值
// atom.ts
export const todoListState = atom<TypeTodo[]>({
    key: 'vOSQ3mQ/$$/todoListAtom',
    default: selector({
        key: 'u17QdkZ/$$/todoListAtom/Default',
        get: async () => {
            const res: any = await ruyi.todo$.get();
            if (res.code === 200) {
                return res.data.list;
            }
            return [];
        }
    }),
    // effects: [
    //     ({ node, onSet }) => {
    //         console.log('==todoList当前节点:', node);
    //         // 设置数据时,监控 atom 的变化
    //         onSet((newValue: any, oldValue: any) => {
    //             console.debug('new:', newValue, 'old:', oldValue);
    //         });
    //     }
    // ]
});

// 使用
const resetTodo = useResetRecoilState(todoListState);
<Button onClick={resetTodo}>重置</Button>
  • www.recoiljs.cn/docs/api-re…
  • 查看状态
    • const [stateValue,setStateValue] = useGetRecoilValueInfo_UNSTABLE()
    • 钩子函数允许组件 “窥视” atom 或者 selector 的当前状态、值和其他信息
    • recoiljs.org/docs/api-re…
  • 刷新状态
    • const stateValue = useRecoilRefresher_UNSTABLE()
    • 钩子返回一个回调,可以使用选择器调用该回调以清除与其关联的任何缓存。
const refreshTodo = useRecoilRefresher_UNSTABLE(todoListState);
<Button onClick={refreshTodo}>刷新</Button>

异步

  • Loadable
  • Loasable对象代表recoil中atom和selector当前的状态,此状态可能有一个可用值,也可能处于错误状态,或者是仍处于 pending 状态的异步解析。
    • loadable.state(loading | hasValue | hasError)
    • loadable.contents
  • 读和写
    • const [loadable,setState] = useRecoilStateLoadable(recoilState)
    • 可用于读取异步 selector 的值
// atom 
export const getCurrentUserState = selector({
    key: 'vng6ukl/$$/getCurrentUserState',
    get: async () => {
        const { principal$ } = ruyi;
        return await principal$.get();
    }
});
export const currentUserState = atom({
    key: '0veg2jy/$$/currentUserState',
    default: getCurrentUserState
});

const [{ state, contents }, setCurrentState] = useRecoilStateLoadable(currentUserState);

setCurrentState({});

回调

  • 回调
    • const [loadable,setState] = useRecoilCallback(recoilState)
    • 这个钩子类似于 useCallback(),但将为你的回调提供一个 API,以便与 Recoil 状态一起工作。这个钩子可以用来构造一个回调,这个回调可以访问 Recoil 状态的只读 Snapshot,并且能够异步更新当前的 Recoil 状态
const logCartItems = useRecoilCallback(({ snapshot }) => async (num:number) => {
    console.log(num);
    const release = snapshot.retain();
    console.log('snapshot:', snapshot.isRetained(), snapshot.getID(),snapshot.getLoadable(langState).contents);
    const numItemsInCart = await snapshot.getPromise(cartState(num));
    console.log('购物车中内容:', numItemsInCart);

    setTimeout(() => {
        release();
        console.log('snapshot:', snapshot.isRetained(), snapshot.getID(),snapshot.getLoadable(cartState(num)).contents);
    },3000)
})
  • www.recoiljs.cn/docs/api-re…
  • 事物
    • const loadable = useRecoilTransaction_UNSTABLE(recoilState)
    • 创建一个事务回调,可用于以安全、简单和高效的方式自动更新多个原子。 为事务提供回调作为可以get()或set()多个原子的纯函数。 事务类似于设置 Recoil 状态的“更新器”形式,但可以在多个原子上操作。 写入对来自同一事务中的后续读取是可见的。
// 事物
const doLogin = useRecoilTransaction_UNSTABLE(({get, set}) => (status) => {
    const user = get(userInfoState);
    const isLogin = get(loginStatusState);
    if (status) {
        set(userInfoState, { ...user, 'score': user.score + 10 });
    }

    set(loginStatusState, !isLogin);
    // 设置 cookie
    // 更新 token	
    // 记录 IP 地址
    // 记录登录时间
    // ......
});

快照

  • Snapshot (快照) 是Recoil用来记录状态的对象
  • 是 Recoil atoms 状态的一个不可改变的快照。它的目的是规范用于观察、检查和管理全局 Recoil 状态的 API。对于开发工具、全局状态同步、历史导航等大部分需求,它都是很有用的。
  • 访问快照
    • useRecoilCallback() - 异步访问
const logCartItems = useRecoilCallback(({ snapshot }) => async (num:number) => {
    console.log(num);
    const release = snapshot.retain();
    console.log('snapshot:', snapshot.isRetained(), snapshot.getID(),snapshot.getLoadable(langState).contents);
    const numItemsInCart = await snapshot.getPromise(cartState(num));
    console.log('购物车中内容:', numItemsInCart);

    setTimeout(() => {
        release();
        console.log('snapshot:', snapshot.isRetained(), snapshot.getID(),snapshot.getLoadable(cartState(num)).contents);
    },3000)
})
  • useRecoilSnapshot() - 同步访问
const snapshot: any = useRecoilSnapshot();

  • www.recoiljs.cn/docs/api-re…
  • useRecoilTransactionObserver_UNSTABLE() - 订阅快照
  • 快照导航(跳转到某个具体的状态)
    • useGotoRecoilSnapshot()
    • 函数返回一个以 Snapshot 作为参数的回调函数,并且将更新当前的 状态以匹配 atom 状态。
const TimeTravelObserver: React.FC<TimeTravelObserverProps> = (props) => {
    const [snapshots, setSnapshots] = useState<any>([]);

    const snapshot: any = useRecoilSnapshot();
    const release = snapshot.retain();

    useEffect(() => {
        console.log('snapshot:', snapshot);
        if (snapshots.every((s: any) => s.getID() !== snapshot.getID())) {
            setSnapshots([...snapshots, snapshot]);
        }
    }, [snapshot]);

    const gotoSnapshot = useGotoRecoilSnapshot();

    return (
        <div>
            {/* <Button onClick={ release }>释放</Button> */}
            <Steps direction='vertical' current={snapshots.length}>
                {
                    snapshots.map((item: any, i: number) => (
                        <Step
                            key={Math.random()}
                            title={`Snapshot ${i}`}
                            description={<Button onClick={() => gotoSnapshot(item)}>Restore</Button>}
                        />
                    ))
                }
            </Steps>
        </div>

    );
};

其他

  • 多个RecoilRoot
    • useRecoilStoreID()
    • 在单个recoilRoot下有一个唯一id
const storeId = useRecoilStoreID();
  • recoiljs.org/docs/api-re…
  • useRecoilBridgeAcrossReactRoots()
  • 帮助桥接 Recoil 状态与嵌套的 React root 和渲染器的钩子函数
function Outer1() {
    const [num, change] = useRandomNumber();
    const RecoilBridge = useRecoilBridgeAcrossReactRoots_UNSTABLE();
    const storeId = useRecoilStoreID();

    return (
        <div className='outer'>
            <Button onClick={change} >Outer1 State: {num}</Button>
            <h3>storeId: {storeId.toString()}</h3>

            <RecoilRoot>
                <Inner>
                    <RecoilBridge>
                        <Inner inBridge />
                    </RecoilBridge>
                </Inner>
            </RecoilRoot>
        </div>
    );
};

并发

  • noWait(类似useRecoilValueLoadable()) recoiljs.org/docs/api-re…
  • waitforNone recoiljs.org/docs/api-re…
    • waitForNone() 类似于 waitForAll(),只是它会立即为每个依赖项返回一个 Loadable,而不是直接返回值。它类似于 noWait(),只是它允许同时请求多个依赖项。
  • waitForAny
    • 并发api,有一个异步可用值,就会直接返回,后续的也会被返回
// 第一次返回
[
    {state:'hasValue'},
    {state:'loading'}
]
// 第二次返回
[
    {state:'hasValue'},
    {state:'hasValue'}
]
  • 内部实现 promise.any() recoiljs.org/docs/api-re…
  • waitForAll
    • 并发api,参数会被依赖收集。所有异步都返回才执行,没有中间态,类似于promise.all()
const { contents, state } = useRecoilValueLoadable(
    waitForAll([postDetailState(query?.id), postCommentsState(query?.id)])
);

console.log('contents',contents);

const [detail, comment] = contents;
console.log('===', detail, comment);

image.png

多个RecoilRoot

  • 数据隔离
    • 多个RecoilRoot之间即可以是并排(兄弟),也可以嵌套(父子)
    • 每个RecoilRoot都有自己独立的storeId(useRecoilStoreID())
    • 默认每个store之间的数据是相互隔离的(互不影响)
  • 数据共享
    • override
<RecoilRoot initializeState={ outerState }>
	<ErrorBoundary fallback={(error) => <PageError error={error} />}>
		{/* <RecoilRoot initializeState={ innerState } override={false}> */}
		<ContextProvider value={{}}>
			<ConfigProvider>{props.children}</ConfigProvider>
		</ContextProvider>
		{/* </RecoilRoot> */}
	</ErrorBoundary>
</RecoilRoot>
  - 默认内层RecoilRoot的状态会覆盖外层(默认overridetrue)
  - 可以设置overridefalse时,不能声明initializeState(初始状态)
  • useRecoilBridgeAcrossReactRoots_UNSTABLE()