Recoil 了解一下

简介

主要解决状态共享的问题,推崇状态和派生数据更细粒度控制,Redux 的数据结构是树而 Recoil 是有向图。Recoil 还是一个实验性的解决方案。

状态改变的流向是从图的根( atoms 共享状态),通过纯函数( selectors 派生数据),最后流入组件中。

特性:

  • 共享状态具有与 React 本地状态相同的简单 get / set 接口。"出于兼容性和简便性的考虑,最好使用 React 的内置状态管理功能,而不是外部全局状态(: 好像暗示了什么"
  • 天然支持 suspense,里面关键的两个点,atom 和 selector 都支持返回 loadable。支持 react 后续的 cocurrent 模式,甚至是后续的其他新特性。
  • 定义是增量式和分布式的,通过观察应用程序中的所有状态更改来实现持久性,路由,时间旅行调试或撤消操作,而不会影响代码拆分。
  • 可以用派生数据替换状态,而无需修改使用状态的组件,派生数据可以抹平同步异步的调用差异。
  • 方便 state 持久化,原理是从 atom 处进行持久化操作,提供订阅 atom 变更的钩子,还正在开发直接订阅全部 atom 变更的钩子方法。

重要概念

Atoms

即状态,可更新和订阅,可以用 atom 直接替代 React 本地组件的 state 使用。

// 创建 atom
const fontSizeState = atom({
  key: 'fontSizeState',
  default: 14,
});
// 读写 atom: useRecoilState(相当于useState)
function FontButton() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  return (
    <button onClick={() => setFontSize((size) => size + 1)} style={{fontSize}}>
      Click to Enlarge
    </button>
  );
}

Selectors

是个纯函数,用于处理一个功能或者派生状态。入参是 atom 或其他 selector,依赖的 atom 或 selector 变更时,该 selector 会重新计算。组件也可直接订阅 selector,其改变时也会重新渲染。

selector 的设计是为了避免冗余的状态。不再需要 reducers 去同步状态,取而代之的是根据最小状态集自动计算功能。

从组件角度看,atom 和 selector 有相同的接口,可以替换使用。

// 创建 selector
const fontSizeLabelState = selector({
  key: 'fontSizeLabelState',
  get: ({get}) => { // get 属性是要计算的函数,如果只提供 get 即为只读,会返回一个只读的 RecoilValueReadOnly对象。
    const fontSize = get(fontSizeState); // 可以访问其他 atom 或 selector, 同时会建立依赖关系。
    const unit = 'px';
    return `${fontSize}${unit}`; // 简单的静态依赖,也可以动态依赖
  },
  // 如果也提供了 set,则会返回一个可写的 RecoilState 对象。
});
// 只读 selector
function FontButton() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  const fontSizeLabel = useRecoilValue(fontSizeLabelState); // 只读的 selector 使用 useRecoilState 方法,入参可以是 atom 或 selector
  return (
    <>
      <div>Current font size: ${fontSizeLabel}</div>

      <button onClick={() => setFontSize(fontSize + 1)} style={{fontSize}}>
        Click to Enlarge
      </button>
    </>
  );
}
// 可读写的 selector
// 这个简单的选择器实质上包装了一个原子以添加一个附加字段。
const proxySelector = selector({
  key: 'ProxySelector',
  get: ({get}) => ({...get(myAtom), extraField: 'hi'}),
  set: ({set}, newValue) => set(myAtom, newValue), // 更改会沿数据流图传播回上游。
});
// 更改数据用法,需判断是否是初始值(这个用法有点麻烦呀。。。
import {selector, DefaultValue} from 'recoil';
const transformSelector = selector({
  key: 'TransformSelector',
  get: ({get}) => get(myAtom) * 100,
  set: ({set}, newValue) =>
    set(myAtom, newValue instanceof DefaultValue ? newValue : newValue / 100),
});
// 和 DefaultValue 对应的还有一个重置方法:
resetTemp = useResetRecoilState(tempCelcius);
resetTemp();

动态依赖

依赖是在 selector 计算的时候,根据实际的依赖动态确定的。因此也可以根据先前依赖关系的值,动态使用其他附加依赖。

const toggleState = atom({key: 'Toggle', default: false});
const mySelector = selector({
  key: 'MySelector',
  get: ({get}) => {
    const toggle = get(toggleState);
    if (toggle) { // 动态添加依赖
      return get(selectorA); 
    } else {
      return get(selectorB);
    }
  },
});

异步 Selector

import {selector, useRecoilValue} from 'recoil';

const myQuery = selector({
  key: 'MyDBQuery',
  get: async () => {
    const response = await fetch(getMyRequestUrl());
    if (response.error) {
      throw response.error;
    }
    return response.json(); // 返回一个 Promise
  },
});

function QueryResults() {
	// 配合上 Suspense 使用后,调用上和同步没有区别。
  // 不配合 Suspense 使用,需要自行处理它的三种状态。
  const queryResults = useRecoilValue(myQuery);
  return (
    <div>
      {queryResults.foo}
    </div>
  );
}

function ResultsSection() {
  return (
  	<RecoilRoot> // 必须
	    <ErrorBoundary> // 按需
        <React.Suspense fallback={<div>Loading...</div>}> // 强烈推荐,不然会很麻烦
          <QueryResults />
        </React.Suspense>
      <ErrorBoundary>
    </RecoilRoot>
  );
}

KEY

Atom、 Selector 都要求保证 key 的全局唯一性,甚至着重说了不唯一是个错误的用法。key 可用于调试,持久化以及某些高级 API,这些 API 可查看所有 atom 的图。在 Recoil 中无论 atom 还是 selector,都是注册成一个 node,treestate 中会保存他们对于 key 的依赖,包括组件下游、 node 下游 和 node 上游,这也是为什么要求 key 唯一的原因。每个 node 都是单独声明的,而不是像 redux 或者 mobx 那样耦合在一个对象里面,这种松耦合的方式,也是 vue3 现在做的,比较高级的说法是 runtime tree shaking。

结语

再从以下几个维度看一下这个技术:

  • TypeScript 支持:最新版本0.0.10 已支持。✅
  • 友好的异步支持:支持,且支持 Suspense 使用。✅
  • 同时支持 Class 与 Hooks 组件:只支持 hooks。❌
  • 使用简单:
    - 和 react 本身的 state 理念一致,理解成本低。✅
    - 配合 react16 新特性的强大功能,导致 API 众多,不容易记忆,不知后续会不会优化 API ❌
    - redux ➕ react-redux ➕ redux-sage ➕ reselector ≈ recoil (虽然源码代码量也是相对可观的)

最后,Recoil 还是个在实验阶段,因此文中的一些用法或者说的到的问题,后面很有可能都会变更,各位看官主要感受该技术的定位即可,等正式版发布后,如果有需要会修正文章内容。

附录

不配合 React Suspense 使用异步 selector 的姿势

function UserInfo({userID}) {
  const userNameLoadable = useRecoilValueLoadable(userNameQuery(userID));
  switch (userNameLoadable.state) {
    case 'hasValue':
      return <div>{userNameLoadable.contents}</div>;
    case 'loading':
      return <div>Loading...</div>;
    case 'hasError':
      throw userNameLoadable.contents;
  }
}

支持后续的cocurrent使用姿势

// 并发请求
const friendsInfoQuery = selector({
  key: 'FriendsInfoQuery',
  get: ({get}) => {
    const {friendList} = get(currentUserInfoQuery);
    const friends = get(waitForAll(
      friendList.map(friendID => userInfoQuery(friendID))
    ));
    return friends;
  },
});
// 处理部分数据的UI增量更新
const friendsInfoQuery = selector({
  key: 'FriendsInfoQuery',
  get: ({get}) => {
    const {friendList} = get(currentUserInfoQuery);
    const friendLoadables = get(waitForNone(
      friendList.map(friendID => userInfoQuery(friendID))
    ));
    return friendLoadables
      .filter(({state}) => state === 'hasValue')
      .map(({contents}) => contents);
  },
});
posted @ 2017-01-31 14:20  冰凌哒雪花  阅读(3843)  评论(0编辑  收藏  举报