写点什么

使用 Signia 实现 React 状态管理

作者:高端章鱼哥
  • 2023-08-08
    福建
  • 本文字数:6154 字

    阅读完需:约 20 分钟

写在前面


如果你在最近的过去开发过任何具有相当复杂程度的 React 应用程序,你可能已经了解状态管理如何很快成为一个主要问题。React 提供的原生工具,如 useState 和 useContext ,在尝试实现常见的设计模式时被证明是不够的,比如由多个组件使用和更新的中央共享状态。


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); // Bananafruit.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-react 和 signia-react-jsx 。


signia-react 提供了像 useAtom 和useComputed这样的钩子,它们有助于管理React组件中的本地状态。signia-react还提供了track和 useValue 等实用程序,您可以使用它们为组件提供反应性,但如果使用 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 className="App">        <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 className="App">        <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 运算符并只更新感兴趣的项目。


还要注意每个待办事项列表项如何具有三个键, id 、 text 和 completed 。需要向这个类添加能够修改此状态的函数,即 addItems 、 markItemAsDone 和 setTitle :


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 box 与button一起使用,您可以使用它向列表中添加项目。使用 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 中显示它们。为此,将使用List和 ListItem 组件以及 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 对象,所以也可以调用 addItem 和 markItemAsDone 方法,它会反映在两个列表中。因此,我们有效地实现了来自中央来源的状态共享。

写在最后


在本文中,我们构建了一个使用 Signia 库及其 React 帮助程序来管理状态的应用程序。 useAtom Hook 提供了 useState 的替代方案,而以原子作为类属性的基于类的体系结构提供了一种构造更复杂的状态的方法。


还探索了一种在 React.createContext 和 useContext 的不同组件之间共享公共状态的方法,所有这些都没有反应性的初始设置和 Redux 等库所期望的样板。因此,Signia 可能是您下次构建 React 应用程序时用于状态管理的库。

发布于: 刚刚阅读数: 3
用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
使用 Signia 实现 React 状态管理_React_高端章鱼哥_InfoQ写作社区