乐趣区

关于javascript:Recoil-状态管理方案的浅入浅出

本文作者:江水

背景: RecoilFacebook 推出的一款专门针对 React 利用的状态治理库,在肯定水平上代表了目前的一种发展趋势,在应用时感觉一些理念很先进,能极大地满足作为一个前端开发者的数据需要,本文对 Recoil 的这些个性做一个梳理。

依据官网的介绍,Recoil 的数据定义了一个有向图 (directed graph),状态的变更是通过扭转图的根节点 (atom),再通过纯函数 (selector) 流向 React 组件。

同时 Recoil 的状态定义是增量和分布式的,增量意味着咱们能够在用的时候再定义新的状态,而不用将所有状态提前定义好再生产。分布式意味着状态的定义能够放在任何地位,不用对立注册到一个文件中。这样的益处是一方面能够简化状态的定义过程,另一方面也能够很好地利用在 code-splitting 场景。

在一个利用中开启 Recoil 非常简单,只须要包裹一个 RecoilRoot 即可。

import {RecoilRoot} from 'recoil';

ReactDOM.render(
  <RecoilRoot>
     <App />
  </RecoilRoot>,
  root);

状态定义,原子和选择器

Recoil 容许应用 atomselector 两个函数定义根底和推导状态。

atom 根本用法,这里定义了相干的原子属性,须要应用惟一 key 来形容这个 atomRecoil 中不容许反复的 key 呈现,包含前面提到的 selector

const firstNameAtom = atom({
  key: 'first name atom',
  default: ''
});

const lastNameAtom = atom({
  key: 'last name atom',
  default: ''
});

应用时通过 useRecoilState 这个 hooks 获取状态,能够看到它和 useState 很像,所以能够很轻松地将传统的 React 状态迁徙到 Recoil 中。

 function UserProfile() {-  const [firstName, setFirstName] = useState('');
+  const [firstName, setFirstName] = useRecoilState(firstNameAtom);

  return (<div> { firstName} </div>
  );
}

很多时候咱们只想获取数据而不想批改,或者反之,此时能够用语法糖 useRecoilValueuseSetRecoilState

function UserProfile() {const firstName = useRecoilValue(firstNameAtom);

  return (<div> { firstName} </div>
  );
}

Recoil 会依据哪里用到了这些状态主动建设一种依赖关系,当产生变更时 Recoil 只会告诉对应的组件进行更新。

selector 的用法和 atom 很像,结构一个 selector 至多须要一个惟一的 keyget 函数。

const nameSelector({
  key: 'my name selector',
  get: ({get}) => {return get(firstNameAtom) + ' ' + get(lastNameAtom);
  }
});

selector 中能够读写任意 atom / selector,没有任何限度。只有 get 办法的 selector 是只读的,如果须要可写,也反对传入 set 办法。

const nameSelector({
  key: 'my name selector',
  get: ({get}) => {return get(firstNameAtom) + ' ' + get(lastNameAtom);
  },
  set: ({get, set}, value) => {const names = value.split(' ');
     set(firstNameAtom, names?.[0]);
     set(lastNameAtom, names?.[1]);
  }
});

值得一提的是,selector 反对从网络异步获取数据,这里才是乏味的开始,也是和其余状态治理的最大的不同,Recoil 的状态不仅是纯状态,也能够是来自网络的状态

const userSelector = selector({
  name: 'user selector',
  get: () => {return fetch('/api/user');
  }
});

应用 selector 时和 atom 一样能够通过 useRecoilState, useRecoilValue, useSetRecoilState 这几个 hook。

function App() {const user = useRecoilValue(userSelector);

    ...
}

这样的个性使得咱们的代码很容易重构,如果一开始一个属性是一个 atom, 前面心愿变成一个计算属性,此时能够很轻松地替换这部分逻辑,而无需批改业务层代码。

Recoil 还能够更弱小,用上面一张图能够大抵概括下,其齐全能够当成一个对立的数据抽象层,将后端数据通过 http, ws, GraphQL 等技术映射到前端组件中。

atomFamily selectorFamily 批量创立状态的解决方案

在一些场景中会有须要批量创立状态的状况,咱们会实例化多个雷同的组件,每个组件都须要对应一个本人独立的状态元素,此时就能够应用 xxxFamily api。

const nodeAtom = atomFamily({
  key: 'node atom',
  default: {}});


function Node({nodeId}) {const [node, setNode] = useRecoilState(nodeAtom(nodeId));
}

能够看到,atomFamily 返回的是一个函数,而不是一个 RecoilState 对象。传入不同的 nodeId 会查看是否之前已存在,如果存在则复用之前的,不存在则创立并应用默认值初始化。

同理,对于 selectorFamily

const userSelector = selectorFamily({
  key: 'user selector family',
  get: (userId) => () => {return fetch(`/api/user/${userId}`);
  }
});


function UserDetail({userId}) {const user = useRecoilValue(userSelector(userId));
}

因为批量创立可能会导致内存透露,所以 Recoil 也提供了缓存策略管理,别离为 lru, keep-all, most-recent,能够依据理论须要选取。

Suspense 与 Hooks

上文提到每个 atom, selector 背地能够是本地数据,也能够是网络状态(对,没错,atom 也能够是个异步数据,罕用的如 atom 初始化是个异步,后续变成同步数据),在组件生产时无需关怀背地的理论起源,应用近程数据就像应用本地数据一样轻松。

来看一个一般的获取数据并展现组件的例子。

function getUser() {return fetch('/api/user');
}

function LocalUserStatus() {const [loading, setLoading] = useState(false);
  const [user, setUser] = useState(null);

  useEffect(() => {setLoading(true);
    getUser().then((user) => {setUser(user);
      setLoading(false);
    })
  }, []);

  if (loading) {return null;}

  return (
    <div>
      {user.name}
    </div>
  )
}

对于这种开发习惯 (往往被称为 Fetch-on-Render):咱们须要一个 useEffect 来获取数据,再须要设置一些 loading, error 状态解决边界状态,如果这个数据不是一个放在全局且处在顶层的数据,而是散落在子组件中生产,则每一个应用的中央都要执行相似的逻辑。

上面看下 Recoil 的写法

const localUserAtom = atom({
  key: 'local user status',
  default: selector({   // <-------- 默认值来自 selector
    key: 'user selector',
    get: () => {return fetch('/api/user');
    }
  })   
});


function LocalUserStatus() {const localUser = useRecoilValue(localUserAtom);

    return (
      <div>
        {localUser.name}
      </div>
    )
}

这里在组件层是不关怀数据从哪来的,Recoil 会主动按需申请数据。

相比之下,后者的代码就简洁许多(Render-as-You-Fetch),而且背地并没有创造新的概念,用到的都是 React 原生的个性,这个个性就是 Suspense

如果应用了一个异步的 atomselector,则外层须要一个 Suspense 解决网络未返回时的 loading 状态。也能够套一层 ReactErrorBoundary 解决网络异样的状况。

// UserProfile 中应用了一个须要从网络中加载的数据
function LocalUserStatus() {const user = useRecoilValue(localUserAtom);

  ...
}


function App() {
  return (
    <div>
        <div>
            hello, 内部组件在这里
        </div>

        <Suspense fallback={<Loading />}>
            <LocalUserStatus />
        </Suspense>

        <div> 底部 </div>
    </div>
  );
}

通过把通用的 LoadingError 逻辑剥离进来,使得个别组件内的条件分支缩小 66%,首次渲染即是数据筹备实现的状态,缩小了额定的解决逻辑以及 hooks 过早初始化问题。

hooks 过早初始化问题可参考拙文: [Recoil 这个状态治理库,用起来可能是最爽的
](https://zhuanlan.zhihu.com/p/…)


useRecoilValueLoadable(state) 读取数据,但返回的是个 Loadable

useRecoilValue 不同,useRecoilValueLoadable 不须要外层 Suspense,相当于将边界状况交给用户解决。

Loadable 的对象构造如下:

它的作用就是咱们可能获取到以后数据是 loading, 还是曾经 hasValue, 手动解决这些状态,适宜灵活处理页面渲染的场景。

const userLoadable = useRecoilValueLoadable(userSelector);

const isLoading = userLoadable.state  === 'loading';
const isError = userLoadable.state === 'hasError';
const value = userLoadable.getValue();

Recoil 用来映射内部零碎

在一些场景下咱们心愿 Recoil 可能和内部零碎进行同步,典型的例子例如 react-routerhistory 同步到 atom 中,原生 js 动画库状态和 Recoil 同步,将 atom 和近程 mongodb 同步。通过间接读写 atom 就能间接读写内部零碎,开发效率能够大大提高。

这种场景下能够借助 recoil-sync 这个包,上面列举两个案例。

应用 sharedb + recoil-sync 能够让 atommongodb/postgres 等数据库进行状态同步,从而让近程数据库批改如同本地批改一样不便。

// 对其的批改会实时同步到近程 mongodb 中
const [name, setName] = useRecoilState(nameAtom); 

应用 recoil-syncatompixi.js 动画元素进行状态同步

https://codesandbox.io/s/nice…

此时能够将画布上的一些精灵变成受控模式。

因为同步过程中会产生数据格式校验问题,recoil-sync 应用 @recoiljs/refine 用来提供数据校验和不同版本数据迁徙性能。

Recoil 状态快照

因为状态粒度较细,对于须要批量设置 RecoilState 的场景,RecoilSnapshot 的概念,适宜 ssr 时注入首屏数据,创立快照进行回滚,批量更新等场景。

填充 SSR 的数据

function initState(snapshot) {
  snapshot.set(atoms.userAtom, {name: 'foo',});
  snapshot.set(atoms.countAtom, 0);
}

export default function App() {
  return (<RecoilRoot initializeState={initState}>
      ...
    </RecoilRoot>
  );
}

利用数据回滚

function TimeMachine() {const snapshotRef = useRef(null);
  const [count, setCount] = useRecoilState(countAtom);

  const onSave = useRecoilCallback(({ snapshot}) => () => {snapshot.retain();
      snapshotRef.current = snapshot;
    },
    []);

  const onRevoca = useRecoilCallback(({ gotoSnapshot}) => () => {if (snapshotRef.current) {gotoSnapshot(snapshotRef.current);
      }
    },
    []);

  return (
    <div>
      <button onClick={onSave}>save</button>
      <button onClick={onRevoca}>recova </button>
      <button onClick={() => setCount((v) => v + 1)}> add {count} </button>
    </div>
  );
}

不应用 async-await 也能实现异步转同步代码

React 的世界里始终存在着一种很奇怪的代码技巧,这种技巧可能不利用 generator 或者 async 就能达到异步转同步的性能,在理解 Recoil 的一些用法时我也留意到这种景象,很有意思,这里介绍下:
如果 userSelector 是一个须要从网络中获取的状态,对其的读取可视作一个异步操作,然而在写 selector 时咱们能够以一种同步的形式来写。

const userNameSeletor = selector({
  key: 'user name selector',
  get: ({get}) => {const user = get(userSelector);  <--- 这里背地是个网络申请
    return user.name;
  }
});

这种写法之前呈现过,在组件中应用 selector 时咱们也没有思考其异步性。

function UserProfile() {const user = useRecoilValue(userProfile); <---- 这里背地也是个网络申请
  const userId = user.id;
  return <div>uid: {userId}</div>;
}

在组件中应用时是利用了外层的 Suspense 执行,在上述的 get 回调中外部也隐式地应用了类似伎俩,当产生异步时 get 办法会将 Promise 当成异样抛出,当异步完结时再从新执行这个函数,所以这个函数自身会执行两次,有点黑魔法的感觉,这也同样要求咱们在此时应该保障 get 是一个纯函数。如果一个 selectorget 回调中存在网络申请,那就不再是一个纯函数,此时须要保障:网络申请是在所有异步 selector 执行之后调用

// 正确的用法
const nameSelector = selector({
    key: "name selector",
    get: async ({get}) => {get(async1Selector);
        get(async2Selector);
        await new Promise((resolve) => {setTimeout(resolve, 0);
        });
        return 1;
    }
});

// 谬误的用法
const nameSelector = selector({
    key: "name selector",
    get: async ({get}) => {get(async1Selector);
        await new Promise((resolve) => {setTimeout(resolve, 0);
        });
        get(async2Selector);
        return 1;
    }
});

最初,对于代码直觉,心智累赘

最近很多人会探讨一个库是否适宜引入时会说到这两个词,在对一个库不理解的状况下咱们很容易就说出“这个库太简单了”,“要记忆的 api 太多了”这类的话。在 Recoil 的世界里如果咱们承受了 atom, selector,那么 atomFamily, selectorFamily 也很容易了解。因为曾经习惯了 useState 那么 useRecoilValue, useSetRecoilValue 也很容易接受,都很合乎 hooks 的直觉。

Recoil 的 api 和 react 本身的 useState, useCallback, Suspense 是概念统一的, 二者的应用反而会加深对 react 框架自身的了解,一脉相承,没有引入其余的编程概念,api 虽多但心智累赘并不大。举个反例,如果在 react 中应用 observable 类型的状态治理,我可能会思考 useEffect 在一些场景是否可能按预期工作,尽管某些个性应用起来很难受,但却加深了心智累赘。

如果有误还望斧正。

本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!

退出移动版