关于Recoil的atom跨RecoilRoot交互的二三事

2,416 阅读3分钟

Recoil介绍

recoil是Facebook公司出的react数据流管理方案,采用分散管理原子状态的设计模式,最近公司新项目选了这个,踩了一些坑来和大家分享一下。

提供给直接看结论的同学,完整Demo地址

Recoil的使用

atom

一个 atom 代表一个状态。Atom 可在任意组件中进行读写。读取 atom 值的组件隐式订阅了该 atom,因此任何 atom 的更新都将致使使用对应 atom 的组件重新渲染

import { atom } from "recoil";

const rootState = atom({
    key: "rootState",
    default: "rootState"
});

在组件中使用atom

import { useRecoilState } from "useRecoilState";

function Root() {
    const [root, change] = useRecoilState(rootState);
    return (
        <>
            <h3>Root State: {root}</h3>
            <br />
            <button onClick={() => change(Math.random().toString())}>
                change root
            </button>
        </>
    )}
    </div>
  );
}

RecoilRoot

如果在组件内使用recoil,则需要在其父组件使用RecoilRoot包裹

import { RecoilRoot } from "recoil";

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

RecoilRoot的一个优点是,当RecoilRoot组件被卸载时,内部的atom状态全都会被清除,在项目中组件的切换可以很好的重置数据,不需要做额外的操作。

而且RecoilRoot是可以多个共存的,可以嵌套使用,但彼此数据是隔离的,可以做到每个atom的状态独立,互不干扰。

import { RecoilRoot, useRecoilState } from "recoil";

const App = () => {
    const [root, change] = useRecoilState(rootState);
    return (
        <>
            <h3>Root State: {root}</h3>
            // 此处rootState的变动不会影响到Root内的状态
            <button onClick={() => change(Math.random().toString())}>
                change root
            </button>
            <RecoilRoot>
                <Root />
            </RecoilRoot>
        </>
    )
}

然而,在实际的开发工作中,不太会有这么理想的碎片化状态管理,状态之间一定会有共享的情况,官方为RecoilRoot提供了一个属性override?: boolean,只要为RecoilRoot设置override: false,那当前RecoilRoot就会与离该层最近的RecoilRoot合并,完成数据共享。

import { RecoilRoot, useRecoilState } from "recoil";

const App = () => {
    const [root, change] = useRecoilState(rootState);
    return (
        <>
            <h3>Root State: {root}</h3>
            // 此处rootState会与Root内的状态共享值
            <button onClick={() => change(Math.random().toString())}>
                change root
            </button>
            <RecoilRoot override={false}>
                <Root />
            </RecoilRoot>
        </>
    )
}

但是这样处理,嵌套RecoilRoot内所有使用到的atom都会被上一层级共享,失去了作用域的功能,即使子组件被卸载,内部atom状态也会被缓存住,除非上一级RecoilRoot被卸载。

有什么办法既能跨RecoilRoot共享状态又能享受到卸载组件清除数据的便利性呢?

跨RecoilRoot数据共享实践

在查阅了官方github仓库的issue、stackoverflow等之后,都没发现有遇到与我类似情况的场景,尝试了一些清除数据的方式未果,最后在翻阅Recoil官方文档的时候发现通过Atom Effects与第三方的事件库就可以解决这个问题,实现如下:

import { atom } from "recoil";
import { v4 as uuidv4 } from "uuid";
import { BehaviorSubject } from "rxjs";


export const crossAtom = (key, defaultVal = "") => {
    const myObservable = new BehaviorSubject({ val: defaultVal });
    const reset = () => {
        myObservable.next({ val: defaultVal });
    };

    const atomState = atom({
        key: key,
        default: defaultVal,
        effects_UNSTABLE: [
            ({ onSet, setSelf, resetSelf }) => {
                const uuid = uuidv4();

                const selfObservable = myObservable.subscribe({
                    next: ({ val, uuid: uid }) => {
                        if (uuid !== uid) setSelf(val);
                    }
                });

                onSet((newVal, oldVal) => {
                    if (newVal === oldVal) return;
                    myObservable.next({ val: newVal, uuid });
                });

                return () => {
                    selfObservable.unsubscribe();
                    resetSelf();
                };
            }
        ]
    });

    return {
        atomState,
        reset
    };
};

最后实现的效果:

2021-09-06 17.17.48.gif

demo中有3层RecoilRoot

  • globalState 全局共享状态,子组件被卸载不会被重置,路由切换会被重置
  • rootState 局部状态,该组件被卸载就会重置
  • childState 局部状态,该组件被卸载就会重置

实现原理是每一个atom在组件内使用时,都会生成新的实例,相同的atom都订阅同一个事件源,在触发onSet事件时发布变动事件通知其他相同atom改变,因为会发生自己通知自己变动的情况导致无限循环,所以为每个atom effect生成一个唯一的uuid以作区分,此demo使用了rxjs来处理事件订阅发布,理论上任何其他方案都可替代。

完整Demo地址