Facebook 新一代 React 狀態管理庫 Recoil

ConardLi發表於2020-10-09

React Europe 2020 Conference 上, Facebook 軟體工程師 Dave McCabe 介紹了一個新的狀態管理庫 Recoil

Recoil 現在還處於實驗階段,現在已經在 Facebook 一些內部產品中用於生產環境。畢竟是官方推出的狀態管理框架,之前沒時間仔細研究,藉著國慶期間看了看,給大家分享一下。

State 和 Context 的問題

假設我們有下面一個場景:有 ListCanvas 兩個元件,List 中一個節點更新後,Canvas 中的節點也對應更新。

最常規則做法是將一個 state 通過父元件分發給 ListCanvas 兩個元件,顯然這樣的話每次 state 改變後 所有節點都會全量更新。

當然,我們還可以使用 Context API,我們將節點的狀態存在一個 Context 內,只要 Provider 中的 props 發生改變, Provider 的所有後代使用者都會重新渲染。

為了避免全量渲染的問題,我們可以把每個子節點儲存在單獨的 Context 中,這樣每多一個節點就要增加一層 Provider

但是,如果子節點是動態增加的呢?我們還需要去動態增加 Provider ,這會讓整個樹再次重新渲染,顯然也是不符合預期的。

引入 Recoil

Recoil 本身就是為了解決 React 全域性資料流管理的問題,採用分散管理原子狀態的設計模式。

Recoil 提出了一個新的狀態管理單位 Atom,它是可更新和可訂閱的,當一個 Atom 被更新時,每個被訂閱的元件都會用新的值來重新渲染。如果從多個元件中使用同一個 Atom ,所有這些元件都會共享它們的狀態。

你可以把 Atom 想象為為一組 state 的集合,改變一個 Atom 只會渲染特定的子元件,並不會讓整個父元件重新渲染。

用 Redux 或 Mobx 不可以嗎?

因為 React 本身提供的 state 狀態在跨元件狀態共享上非常苦難,所以我們在開發時一般藉助一些其他的庫如 Redux、Mobx 來幫助我們管理狀態。這些庫目前正被廣泛使用,我們也並沒有遇到什麼大問題,那麼 Facebook 為什麼還要推出一款新的狀態管理框架呢?

使用 Redux、Mobx 當然可以,並沒有什麼問題,主要原因是它們本身並不是 React 庫,我們是藉助這些庫的能力來實現狀態管理。像 Redux 它本身雖然提供了強大的狀態管理能力,但是使用的成本非常高,你還需要編寫大量冗長的程式碼,另外像非同步處理或快取計算也不是這些庫本身的能力,甚至需要藉助其他的外部庫。

並且,它們並不能訪問 React 內部的排程程式,而 Recoil 在後臺使用 React 本身的狀態,在未來還能提供併發模式這樣的能力。

基礎使用

初始化

使用 recoil 狀態的元件需要使用 RecoilRoot 包裹起來:

import React from 'react';
import {
  RecoilRoot,
  atom,
  selector,
  useRecoilState,
  useRecoilValue,
  useSetRecoilState
} from 'recoil';

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

定義狀態

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

首先我們來定義一個 Atom

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

這種方式意味著你不需要像 Redux 那樣集中定義狀態,可以像 Mobx 一樣將資料分散定義在任何地方。

要建立一個 Atom ,必須要提供一個 key ,其必須在 RecoilRoot 作用域中是唯一的,並且要提供一個預設值,預設值可以是一個靜態值、函式甚至可以是一個非同步函式。

訂閱和更新狀態

Recoil 採用 Hooks 方式訂閱和更新狀態,常用的是下面三個 API:

  • useRecoilState:類似 useState 的一個 Hook,可以取到 atom 的值以及 setter
  • useSetRecoilState:只獲取 setter 函式,如果只使用了這個函式,狀態變化不會導致元件重新渲染
  • useRecoilValue:只獲取狀態
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>;
}

派生狀態

selector 表示一段派生狀態,它使我們能夠建立依賴於其他 atom 的狀態。它有一個強制性的 get 函式,其作用與 reduxreselectMobX@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}</>;
}
selector 是一個純函式:對於給定的一組輸入,它們應始終產生相同的結果(至少在應用程式的生命週期內)。這一點很重要,因為選擇器可能會執行一次或多次,可能會重新啟動並可能會被快取。

非同步狀態

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 推薦使用 SuspenseSuspense 將會捕獲所有非同步狀態,另外配合 ErrorBoundary 來進行錯誤捕獲:

function MyApp() {
  return (
    <RecoilRoot>
      <ErrorBoundary>
        <React.Suspense fallback={<div>Loading...</div>}>
          <CurrentUserInfo />
        </React.Suspense>
      </ErrorBoundary>
    </RecoilRoot>
  );
}

總結

Recoil 推崇的是分散式的狀態管理,這個模式很類似於 Mobx,使用起來也感覺有點像 observable + computed 的模式,但是其 API 以及核心思想設計的又沒有 Mobx 一樣簡潔易懂,反而有點複雜,對於新手上手起來會有一定成本。

在使用方式上完全擁抱了函式式的 Hooks 使用方式,並沒有提供 Componnent 的使用方式,目前使用原生的 Hooks API 我們也能實現狀態管理,我們也可以使用 useMemo 創造出派生狀態,RecoiluseRecoilState 以及 selector 也比較像是對 useContext、useMemo 的封裝。

但是畢竟是 Facebook 官方推出的狀態管理框架,其主打的是高效能以及可以利用 React 內部的排程機制,包括其承諾即將會支援的併發模式,這一點還是非常值得期待的。

另外,其本身的分散管理原子狀態的模式、讀寫分離、按需渲染、派生快取等思想還是非常值得一學的。

最後

文章中如有錯誤,歡迎在評論區指正;如果文章對你有幫助,歡迎點贊、評論、分享、希望能幫到更多人。

本文首發於公眾號《code祕密花園》歡迎大家關注,原文:Facebook 新一代 React 狀態管理庫 Recoil

相關文章