从 Recoil 到 Jotai(下)

Photo by Clark Tibbs on Unsplash

从 Recoil 到 Jotai(下)

·

2 min read

接着上一篇,我们来聊聊 RecoilJotai,由于下篇属于 高级用法,我会穿插一些原理以及个人的想法(有错误可以指正)。

selector -> atom

派生状态,顾名思义就是从一个基础的状态,派生为其他更多的状态,有点类似面向对象的继承,其实都是一个意思。

  1. recoil
const todoListFilterState = atom({
  key: 'TodoListFilter',
  default: 'Show All',
});

const filteredTodoListState = selector({
  key: 'FilteredTodoList',
  get: ({get}) => {
    const filter = get(todoListFilterState);
    const list = get(todoListState);

    switch (filter) {
      case 'Show Completed':
        return list.filter((item) => item.isComplete);
      case 'Show Uncompleted':
        return list.filter((item) => !item.isComplete);
      default:
        return list;
    }
  },
});
  1. jotai
import { selectAtom } from 'jotai/utils'

import { atom } from 'jotai'


export const baseAtom = atom(Math.random());


export const oneAtom = selectAtom(baseAtom, val => val);



export const twoAtom = selectAtom(baseAtom, val => val*2);

但是 recoiljotai 的还是有一些区别的,可以看到代码中, recoil 内部有 get 方法可以直接获取到多个原子的状态。但是 jotai 仅能获取当前传入的。

那我们有办法吗?请看下文:

export const baseAtom = atom(Math.random());

export const baseAtom1 = atom(Math.random());



export const oneAtom = selectAtom(baseAtom, val => val);



export const twoAtom = atom<any, any, any>(get => get(baseAtom)*8, (get, set, params) => {
 set(baseAtom, params)
})

我们的 atom 具备四个能力:

  • 原始原子(拥有初始值,仅作存储,可以随意塞值)

  • 读原子

  • 写原子

  • 读写原子

其实这里在使用上还是有困惑点的,也就是 selectAtom 的能力其实仅仅就是个派生+监听+equal ,不具备能力。

这点已经询问过 dai-shi 了:

GitHub Discussion

atomFamily -> atomFamily

atomFamily 的定义就是返回一个带有写原子能力的函数。

recoiljotai ,前者自己做了 memo,后者需要手动去做 memo

  1. recoil
const elementPositionStateFamily = atomFamily({
  key: 'ElementPosition',
  default: : param => defaultBasedOnParam(param)
});

function ElementListItem({elementID}) {
  const position = useRecoilValue(elementPositionStateFamily(elementID));
  return (
    <div>
      Element: {elementID}
      Position: {position}
    </div>
  );
}
  1. jotai
import { atom } from 'jotai'

import { atomFamily } from 'jotai/utils'

import deepEqual from 'fast-deep-equal'

const fooFamily = atomFamily((param) => atom(param), deepEqual)

有同学可能对于此处的 deepEqual 不是很理解。

简单理解就是:根据请求的 param 去作 Map 存储,当相同的时候就从缓存里拿,不再重新创建新的原子,这个时候 params 为 object 时候,尤为有效。

但是存在内存泄漏风险,不论有没有添加 deepEqual 函数,因为始终是存在 map 里的,所以我们要在恰当的时机去清除。否则就会内存泄漏

当然我是有一个 💡,自己撸一个 atomFamily ,结合 Provider 的能力,变更 Provider 之后,自动清理内部的 Map ,但是这跟设计又有点强耦合了(:有待商榷


那么 recoil 是如何实现的呢?

本质还是借助 Map 去实现的,但是 key 有些不一样,你可以自己传入一个 keyMapper 函数过滤键值。

其实两者有异曲同工之妙啊。。。大家有没有发现。

Loadable

  1. recoil

useRecoilStateLoadable | useRecoilValueLoadable

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;
  }
}
  1. jotai
import { loadable } from "jotai/utils"

const asyncAtom = atom(async (get) => ...)

const loadableAtom = loadable(asyncAtom);
// Does not need to be wrapped by a <Suspense> element

const Component = () => {

  const [value] = useAtom(loadableAtom)

  if (value.state === 'hasError') return <Text>{value.error}</Text>

  if (value.state === 'loading') {

    return <Text>Loading...</Text>

  }

  console.log(value.data) // Results of the Promise

  return <Text>Value: {value.data}</Text>

}

相比较于 recoiljotai 的组合看起来更便捷,但是 recoil 同样方便,直接使用特定的 API 集成。

useRecoilRefresher_UNSTABLE -> atomWithRefresh

  1. recoil
const myQuery = selector({
  key: 'MyQuery',
  get: () => fetch(myQueryURL),
});

function MyComponent() {
  const data = useRecoilValue(myQuery);
  const refresh = useRecoilRefresher_UNSTABLE(myQuery);

  return (
    <div>
      Data: {data}
      <button onClick={() => refresh()}>Refresh</button>
    </div>
  );
}
  1. jotai
import { atom, Getter } from 'jotai'

export function atomWithRefresh<T>(fn: (get: Getter) => T) {

  const refreshCounter = atom(0)

  return atom(

    (get) => {

      get(refreshCounter)

      return fn(get)

    },

    (_, set) => set(refreshCounter, (i) => i + 1)

  )
}
import { atomWithRefresh } from 'XXX'

const postsAtom = atomWithRefresh((get) =>

  fetch('https://jsonplaceholder.typicode.com/posts').then((r) => r.json())

)

好了,到此为止,常用的 API 类的过渡差异基本结束,下一篇我会从《jotai 实战》开始入手,从一个应用常见的数据存储处理、回显入手,讲一下 jotai 的实战和常见的坑。