使用 Signia 實現 React 狀態管理

原文作者:Kapeel Kokane

原文地址:https://blog.logrocket.com/implement-react-state-management-signia/

翻譯:一川

寫在前面

如果你在最近的過去開發過任何具有相當複雜程度的 React 應用程序,你可能已經瞭解狀態管理如何很快成爲一個主要問題。React 提供的原生工具,如 useStateuseContext ,在嘗試實現常見的設計模式時被證明是不夠的,比如由多個組件使用和更新的中央共享狀態。

Redux 是幫助解決這個問題的最受歡迎的庫; 它運行了幾年,爲了克服它存在的小差距,一個偉大的生態系統以ReselectRedux-Saga等庫的形式圍繞它發展起來。最近,MobXZustandJotai等其他替代品越來越受歡迎。在本文中,我們將瞭解 Signia,這是一個使用信號來解決相同問題的狀態管理庫。

什麼是 Signia?

正如tldraw團隊在公告博客文章中提到的,“Signia是一個原始庫,用於處理細粒度的反應值,稱爲signals,使用基於邏輯時鐘的新惰性反應模型”。

簡單來說,Signia 使用稱爲signals的基元進行狀態管理,它可以通過執行增量計算來有效地計算計算值。此外,藉助爲整個事務的回滾提供支持的內部時鐘,如果需要,它們可以實現事務的概念。

雖然核心庫與框架無關,但 tldraw 團隊還發布了一組 React 綁定,這使得將 Signia 集成到 React 應用程序中變得輕而易舉。

signals到底是什麼?

在進入 Signia 的功能之前,讓我們先了解一下信號的概念是什麼。根據官方文件,“signals是一個隨時間變化的值,其變化事件會引發副作用”。換句話說,signals是一個純粹的、無功的值,可以觀察到變化。然後,信號庫負責觀察這些變化,通知訂閱者,並觸發所需的副作用。

從理論上講,signals有點類似於RxJS庫提供的可觀察量的概念,但有一些根本區別。其中之一是需要創建訂閱並傳遞迴調函數來訪問可觀察量的值。

Signia核心概念

讓我們回顧一下理解 Signia 所必需的一些概念。

Atom 原子

Signia 中的 Atom 表示對應於根狀態的signals,即應用的真實來源。可以讀取和更新其值,也可以在此基礎上構建以創建計算值。

創建原子

要創建 AtomSignia 庫提供了 atom 函數:

import { atom } from 'signia'
const fruit = atom('fruit''Apple');

上面的代碼創建一個名爲 fruit 的信號,值爲Apple。還將fruit作爲第一個參數傳遞給 atom 函數,因爲它有助於調試目的。

更新原子

爲了更新 Atom,使用 set 函數,如下所示:

fruit.set('Banana');
console.log(fruit.value); // Banana
fruit.set((current) => current + 's');
console.log(fruit.value); // Bananas

React setState 函數類似,有一個接受函數作爲參數的set的替代版本。然後,它使用信號的當前值調用該函數並計算更新的值。

計算信號

計算信號來自原子,因此依賴於它們;每當它們所依賴的原子發生變化時,它們的值就會重新計算。

創建computed信號

您可以使用 computed 函數創建計算信號,如下所示:

import { computed, atom } from 'signia'
const fruits = atom('fruits''Apples')
const numberOf = atom('numberOf', 10)
const display = computed('display'() ={
    return `${numberOf.value} ${fruits.value}`
})
console.log(display.value) // 10 Apples

更新computed信號

沒有更新計算信號的直接方法。但是,更新其任何根原子都會自動更新計算出的信號:

fruits.set('Bananas')
console.log(display.value) // 10 Bananas

如上所示,計算信號的值被更新以反映在fruits的根原子上設置的最新值。

React 綁定 Signia

到目前爲止,我們回顧的代碼示例是通用的,使用 Signia 核心庫。但是,如前所述,tldraw 團隊還發布了一組 React 綁定,可以更輕鬆地將 Signia 集成到React應用程序中。官方的 React 綁定以兩個包的形式提供,即 signia-reactsignia-react-jsx

signia-react 提供了像 useAtomuseComputed這樣的鉤子,它們有助於管理React組件中的本地狀態。signia-react還提供了trackuseValue 等實用程序,您可以使用它們爲組件提供反應性,但如果使用 signia-react-jsx 庫,則不需要。

signia-react-jsx 提供導致所有功能組件變得跟蹤和響應的配置選項。它還解包每個信號,因此不需要將信號包裝在 useValue 中。現在,創建一個使用Signia進行狀態管理的 React 待辦事項列表應用程序。

上手體驗 Signia

SigniaVite有開箱即用的支持,所以將使用 Vite 作爲打包器。若要創建新的 Vite 項目,請運行以下命令:

npm create vite@latest

當界面出現時,爲新項目提供一個名稱,選擇 React 作爲框架,然後選擇 TypeScript 作爲語言。創建項目時,應看到類似於以下內容的內容:

我們需要在創建項目的目錄中工作,在例子中是 todo-list-signia 目錄。

設置 Signia

現在,讓我們安裝特定於Signia的庫:

npm install --save signia-react signia-react-jsx

我們將爲組件設置響應式,這樣就不需要手動將每個組件包裝在 track 函數中。爲了進行設置,在新創建的樣板 Vite 項目中打開 tsconfig.json 文件,並將以下代碼添加到 compilerOptions 對象:

"compilerOptions"{
  "jsx""react-jsx",
  "jsxImportSource""signia-react-jsx"
}

現在,我們可以開始在樣板中使用 Signia

設置 Chakra UI

我們還安裝 Chakra UI 組件庫,將使用它來構建 UI 組件,使它們看起來乾淨有序。要安裝 Chakra UI 及其對等依賴項,請運行以下命令:

npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion react-icons

App.tsx 中進行以下更改:

import { ChakraProvider } from '@chakra-ui/react'
function App() {
  return (
   <ChakraProvider>
      <div class>
        <Heading>Testing Vite!</Heading>
        <Button colorScheme='blue'>Button</Button>
      </div>
    </ChakraProvider>
  )
}

接下來,使用以下命令運行本地開發服務器:

npm run dev

可以看到該應用程序在 localhost 上啓動並運行,顯示以下內容:

使用 Signia 測試響應式

在創建實際應用程序之前,測試一下是否正確設置了所有內容。將創建一個簡單的計數器應用,該應用使用 Signia 進行狀態管理。我們將創建一個 useAtom 的局部狀態變量,該變量將保存計數的值,並在每次單擊按鈕時添加一個增量函數:

import { useAtom } from 'signia-react'
...
function App() {
  const count = useAtom('count', 0);
  const onButtonClick = () ={
    count.set(count.value + 1);
  }
  return (
    <ChakraProvider>
      <div class>
        <Heading>Counter value: {count.value}</Heading>
        <Button colorScheme='blue' onClick={() => onButtonClick()}>Increment</Button>
      </div>
    </ChakraProvider>
  )
}

當我們單擊按鈕時,可以看到計數器值已正確更新。因此,設置按預期工作:

設計狀態

現在可以將簡單值存儲爲 Signia atom ,繼續下一步,給待辦事項列表應用程序設計狀態。要求是存儲兩個實體,即項列表和列表標題。可以使用 Signia 團隊推薦的基於類的設計,並創建兩個單獨的 Atom 來存儲這些實體。該類將如下所示:

class Todo {
  metadata = atom('metadata'{
    title: 'Groceries',
  })
  items = atom('items'{
    1: {
      id: 1,
      text: 'Milk',
      completed: false,
    }
  })
}

請注意,items class 屬性是一個包含與各個項目對應的其他對象的對象,這將有助於我們有效地更新狀態。不需要遍歷項目來尋找的項目,可以在項目上使用 spread 運算符並只更新感興趣的項目。

class Todo {
  ...
  addItem(todoText: string) {
    const listItem = {
    id: Date.now(),
    text: todoText,
     completed: false,
    }
    this.items.update((items) =({ ...items, [listItem.id]: listItem }))
  }
  markItemAsDone(itemId) {
    const updatedItem = { ...this.items.value[itemId], completed: true }
    this.items.update((items) =({ ...items, [itemId]: updatedItem }))
  }
  setTitle(title: string) {
    this.metadata.update((metadata) =({ ...metadata, title }))
  }
}

上面的代碼實現UX所需的所有最小功能。

創建用戶界面

對於待辦事項列表應用的 UI,將在頂部顯示標題。若要實現重命名列表的功能,只需提供一個 edit 按鈕並調用已在狀態類中定義的 setTitle 函數。

Title 下方,可以將 input boxbutton一起使用,您可以使用它向列表中添加項目。使用 Chakra UI,標題的代碼以及輸入框如下所示:

<Heading>Todo Title</Heading>
<InputGroup size='md' mt='2rem'>
  <Input
    pr='4.5rem'
    type={'text'}
    value={todoText.value}
    onChange={onTodoItemChange}
    placeholder='Enter item to add'
  />
  <InputRightElement width='4.5rem'>
    <Button h='1.75rem' size='sm' onClick={onAddClick}>
      Add
    </Button>
  </InputRightElement>
</InputGroup>

爲了掌握 React 組件內部的狀態,我們必須實例化 Todo 類。爲此,使用 useMemo Hook 創建狀態的記憶版本,如下所示:

const useNewTodo = () => useMemo(() => new Todo()[])
We can now use this custom hook inside of the App component:
function App() {
  const todo = useNewTodo()
  ...
}

還需要創建一個本地狀態變量,該變量將跟蹤在 input內鍵入的文本。可以利用 useAtom 來實現此目的:

const todoText = useAtom('todoText''');
const onTodoItemChange = (e) ={
  todoText.set(e.target.value);
}

還需要兩個處理程序,一個用於處理 todo 項的添加,另一個用於將其標記爲完成:

const onAddClick = (e) ={
  todo.addItem(todoText.value);
  todoText.set('');
}
const onDoneClick = (id) ={
  todo.markItemAsDone(id);
}

單擊添加按鈕時,在實例化的狀態類上調用 addItem 方法。選中該複選框後,使用 ID 調用 markItemAsDone 方法。

還剩下一件事。

循環訪問待辦事項列表並在 UI 中顯示它們。爲此,將使用ListListItem 組件以及 Object.values 幫助程序來迭代對象值:

<List spacing={3} textAlign={'left'} mt='2rem'>
  {Object.values(todo.items.value).map((item) =(
    <ListItem key={item.id} alignItems={'center'}>
      <Checkbox disabled={item.completed} checked={item.completed} mt={'4px'} mr={2} onChange={() => onDoneClick(item.id)} />
      <Text as={item.completed ? 's' : 'b'}>{item.text}</Text>
    </ListItem>
  ))}
</List>

這樣就完成了最小待辦事項列表應用正常工作所需的所有代碼更改。您可以檢查完整的代碼更改集,甚至可以通過克隆此 GitHub(https://github.com/kokanek/todo-list-with-signia) 存儲庫自行運行它。

測試 UI

讓我們測試一下代碼更改。當第一次運行應用程序時,可以看到待辦事項列表中存在的 Milk 項,因爲在以下狀態下對其進行了硬編碼:

UI 按預期工作,可以根據需要添加更多任務。

在 React 組件之間共享狀態

需要探索的最後一件事是在不同的 React 組件之間共享狀態。在本教程中構建的示例在同一文件中具有狀態類以及該狀態的使用者。

但是,在現實生活中的用例中,狀態和消費的存儲點相距甚遠。在這些場景中,如何管理共享狀態?Signia 建議使用 React.context。首先使用狀態類創建一個上下文,然後將整個應用程序包裝在該上下文提供程序中,將實例化的狀態類作爲值傳遞:

const TodoContext = React.createContext<Todo | null>(null)
class TodoHelper {
   static useNewTodo = () ={
        const todoState = useMemo(() => new Todo()[])
        return todoState
    }
}
const App = () ={
    const todo = TodoHelper.useNewTodo()
    return (
        <TodoContext.Provider value={todo}>
        ...other components get access to the state
        </TodoContext.Provider>
    )
}

在示例中進行這些更改並進行測試。爲此,在 App 組件中進行上述更改。然後,創建一個名爲 TodoList.jsx 的新文件,並複製代碼以呈現其中的列表項。還將代碼用於從此文件的上下文中使用狀態對象:

import { TodoContext } from './App';
const useTodoFromContext = () ={
  const doc = useContext(TodoContext)
  if (!doc) throw new Error('No document found in context')
  return doc
}
export function TodoList() {
  const todo = useTodoFromContext();
  return (
    <List spacing={3} textAlign={'left'} mt='2rem'>
      {Object.values(todo.items.value).map((item) =(
       <ListItem key={item.id} alignItems={'center'}>
         <Checkbox disabled={item.completed} checked={item.completed} mt={'4px'} mr={2} />
         <Text as={item.completed ? 's' : 'b'}>{item.text}</Text>
        </ListItem>
      ))}
    </List>
  )
}

useTodoFromContext 幫助程序負責獲取上下文並將狀態的最新實例化返回給此組件。現在,將這個組件放在 App.tsx 文件中的藍色 <div> 中。可以將其放置在 UX 中的任何位置,甚至可以在新路線上。

現在,當添加新的待辦事項時,看到從上下文中讀取此狀態的 TodoList 組件也顯示添加到列表中的最新項:

在上面的演示中,正在讀取 TodoList 組件中的列表項。因爲可以從上下文訪問 todo 對象,所以也可以調用 addItemmarkItemAsDone 方法,它會反映在兩個列表中。因此,我們有效地實現了來自中央來源的狀態共享。

寫在最後

在本文中,我們構建了一個使用 Signia 庫及其 React 幫助程序來管理狀態的應用程序。 useAtom Hook 提供了 useState 的替代方案,而以原子作爲類屬性的基於類的體系結構提供了一種構造更復雜的狀態的方法。

還探索了一種在 React.createContextuseContext 的不同組件之間共享公共狀態的方法,所有這些都沒有反應性的初始設置和 Redux 等庫所期望的樣板。因此,Signia 可能是您下次構建 React 應用程序時用於狀態管理的庫。

歡迎關注筆者公衆號「宇宙一碼平川」,助你技術路上一碼平川。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/LLoUNaBgeF_A_pJkf2lkYw