Recoil 新一代的 React 函數語言程式設計 狀態管理工具

mit發表於2022-06-10

我為什麼要用 Recoil?

起因是最近重構了一個 props 傳遞層級非常深,元件之間狀態通訊非常頻繁的詳情頁面。整個程式碼梳理下來,很難維護,重構時考慮,做元件細分,把資料單拎出來,對資料做一個鬆散管理。

對比了下之前用過的 Redux、Mobx 感覺有點重,因為專案 純 ts + Func hooks 形式重構,應用這兩種比較常規的狀態管理工具有兩個弊端:

  1. 對於專案程式碼侵入比較大,要編寫大量的狀態管理程式碼(dispatch、action、reducer)
  2. 因為是外部庫,它們並不能訪問 React 內部的排程程式

主要還是覺得這兩個庫,對於專案使用來說,有點重了,沒必要為了一個不算很複雜的資料管理和通訊引入並不算小的 npm 依賴包。起初考慮用 Context 來處理,但是多個元件訂閱要寫多個 Provider 或者有一個根元件的 Provider 來接入資料,這就有會導致很多不必要的重繪和程式碼量。剛好有同事提了一嘴 Recoil(fb 自己為 react 提供的狀態管理工具庫),看了下資料,感覺跟自己的專案契合度還不錯,遂試用了下,效果不錯。


簡單的介紹下 Recoil

Recoil是FaceBook公司提出的狀態管理方案(個人感覺像是給 react Func Comps 量身打造的輕量級狀態管理工具)。
我們都知道React強調的是immuteable,而Recoil強調的同樣也是immuteable,immuteable給帶來的好處就是增強元件整體的應用效能。對於 immuteable 不是很清楚的,建議查閱相關文件,這裡不做闡述。

Recoil採用分散管理原子狀態的設計模式.

Recoil提出了一個新的管理狀態單位Atom(原子化),它是可更新和訂閱的,當一個Atom更新之後,每個訂閱它的元件都會與之更新重新渲染,如果多個元件使用同一個Atom,那麼這些元件將會共享他們的狀態。

image.png

從上圖不難看出,它的核心理念是原子化拆分資料,通過訂閱釋出的模型對資料進行管理。

Recoil基礎

初始化

使用Recoil的元件需要使用RecoilRoot元件包裹起來

import React from 'react';
import { RecoilRoot } from 'recoil';

function App() {
  return (
    <RecoilRoot>
      <CharacterCounter />
    </RecoilRoot>
  );
}

定義狀態

上面我們已經提到了 Atom 的概念, Atom 是一種新的狀態,但是和傳統的 state 不同,它可以被任何元件訂閱,當一個 Atom 被更新時,每個被訂閱的元件都會用新的值來重新渲染。

export const pageInfoState = atom({
  key: 'pageInfoState',
  default: {}
});

其中 key 必須在 RecoilRoot 作用域內唯一。
default 定義預設值。

訂閱和更新狀態

useRecoilState:類似 useState 的一個 Hook,可以取到 atom 的值以及 setter 函式。
useSetRecoilState:只獲取 setter 函式,如果只使用了這個函式,狀態變化不會導致元件重新渲染。
useRecoilValue:只獲取狀態。
useResetRecoilState: 重置狀態。

import { nameState } from './store'
// useRecoilState
const NameInput = () => {
  const [name, setName] = useRecoilState(nameState);
  const onChange = (event) => {
   setName(event.target.value);
  };
  return <>
   <input type="text" value={name} onChange={onChange} />
   <div>Name: {name}</div>
  </>;
}

// useRecoilValue
const SomeOtherComponentWithName = () => {
  const name = useRecoilValue(nameState);
  return <div>{name}</div>;
}

// useSetRecoilState  
const SomeOtherComponentThatSetsName = () => {
  const setName = useSetRecoilState(nameState);
  return <button onClick={() => setName('Jon Doe')}>Set Name</button>;
}

// useReSetRecoilState  
const SomeOtherComponentThatSetsName = () => {
  const ressetName = useSetRecoilState(nameState);
  return <button onClick={ressetName}>Set Name</button>;
}

從上面我們可以看到我們取值和改變值是使用 hooks 的形式,對於 react 函式元件來說是非常友好的,程式碼侵入非常小。

派生狀態

selector 表示派生狀態,它使我們能夠建立依賴於其他 atom 的狀態(也可以進行一些計算操作)。它有一個強制性的 get 函式,有點類似 Vue 中的 computed 相似.

const lengthState = selector({
  key: 'lengthState', 
  get: ({get}) => {
    const text = get(nameState);
    return text.length;
  },
});

function NameLength() {
  const length = useRecoilValue(charLengthState);
  return <>Name Length: {length}</>;
}

非同步狀態

Recoil 提供了通過資料流圖將狀態和派生狀態對映到 React 元件的方法。真正強大的功能是圖中的函式也可以是非同步的。這使得我們可以在非同步 React 元件渲染函式中輕鬆使用非同步函式。使用 Recoil,你可以在選擇器的資料流圖中無縫地混合同步和非同步功能。只需從選擇器 get 回撥中返回 Promise ,而不是返回值本身。

例如下面的例子,如果使用者名稱儲存在我們需要查詢的某個資料庫中,那麼我們要做的就是返回一個 Promise 或使用一個 async 函式。如果 userID 發生更改,就會自動重新執行新查詢。結果會被快取,所以查詢將僅對每個唯一輸入執行一次(所以一定要保證 selector 純函式的特性,否則快取的結果將會和最新的值不一致)。

const userNameQuery = selector({
  key: 'userName',
  get: async ({get}) => {
    const response = await myDBQuery({
      userID: get(currentUserIDState),
    });
    return response.name;
  },
});

function CurrentUserInfo() {
  const userName = useRecoilValue(userNameQuery);
  return <div>{userName}</div>;
}

總結

使用下來以後,感覺 Recoil 的分散式狀態管理,有點像 observable + computed 的模式。具體原始碼我還沒有去看過,找個時間看過之後,再補一個原始碼解讀。使用方式上,完全是迎合函式式元件 Hooks 的使用方式,沒有提供 Component 的使用方式。

官方出品必屬精品,官方文件所宣揚的高效能和可利用 React 內部的排程機制,包括之前承諾的併發模式,還是比較值得期待的(聽聽就好了)。主要還是看跟自己專案的契合度,合適你就用,工程和專案複雜度上來了,該穩重還是選穩重的狀態管理方案吧,反正我下一階段打算,把 Recoil 這玩意兒,在專案裡面鋪開體驗了。

Recoil 官方文件連結

https://recoil.js.cn/

相關文章