Valtio

Valtio

代理状态变得简单。

Valtio API 是最小化、灵活、无偏见的,并且带有一丝魔力。Valtio 的代理将您传递给它的对象转换为自感知代理,允许在更新状态时进行细粒度订阅和响应性。在 React 中,Valtio 在渲染优化方面表现出色。它与 Suspense 和 React 18 兼容。Valtio 在原生 JavaScript 应用程序中也是一个可行的选择。

安装

npm install valtio

待办事项应用示例

1. proxy

让我们通过用 React 和 TypeScript 编写一个简单的待办事项应用来学习 Valtio。我们将从使用 proxy 创建一些状态开始。

import { proxy, useSnapshot } from 'valtio'

type Status = 'pending' | 'completed'
type Filter = Status | 'all'
type Todo = {
  description: string
  status: Status
  id: number
}

export const store = proxy<{ filter: Filter; todos: Todo[] }>({
  filter: 'all',
  todos: [],
})

2. useSnapshot

要访问此存储中的数据,我们将使用 useSnapshot。当 "todos" 或 "filter" 属性更新时,Todos 组件将重新渲染。我们添加到代理中的任何其他数据将被忽略。

const Todos = () => {
  const snap = useSnapshot(store)
  return (
    <ul>
      {snap.todos
        .filter(({ status }) => status === snap.filter || snap.filter === 'all')
        .map(({ description, status, id }) => {
          return (
            <li key={id}>
              <span data-status={status} className="description">
                {description}
              </span>
              <button className="remove">x</button>
            </li>
          )
        })}
    </ul>
  )
}

3. actions

最后,我们需要创建、更新和删除我们的待办事项。为此,我们只需在我们创建的存储上(而不是快照上)改变属性。通常,这些变化被包装在称为 actions 的函数中。

const addTodo = (description: string) => {
  store.todos.push({
    description,
    status: 'pending',
    id: Date.now(),
  })
}

const removeTodo = (id: number) => {
  const index = store.todos.findIndex((todo) => todo.id === id)
  if (index >= 0) {
    store.todos.splice(index, 1)
  }
}

const toggleDone = (id: number, currentStatus: Status) => {
  const nextStatus = currentStatus === 'pending' ? 'completed' : 'pending'
  const todo = store.todos.find((todo) => todo.id === id)
  if (todo) {
    todo.status = nextStatus
  }
}

const setFilter = (filter: Filter) => {
  store.filter = filter
}

最后,我们将这些 actions 连接到我们的输入和按钮 - 查看下面的演示以获取完整代码。

<button className="remove" onClick={() => removeTodo(id)}>
  x
</button>

Codesandbox 演示

在组件外部改变状态

在我们的第一个待办事项应用中,Valtio 启用了变化,而无需担心性能或"破坏"React。useSnapshot 将这些变化转换为不可变的快照并优化渲染。但我们也可以轻松使用 React 自己的状态处理。让我们为我们的待办事项应用添加一些复杂性,看看 Valtio 还提供什么。

我们将为待办事项添加一个 "timeLeft" 属性。现在每个待办事项都会倒计时到零,如果未及时完成则变为"逾期"。

type Todo = {
  description: string
  status: Status
  id: number
  timeLeft: number
}

在其他更改中,我们将添加一个 Countdown 组件来显示每个待办事项的倒计时。我们使用将嵌套代理对象传递给 useSnapshot 的高级技术。或者,通过将待办事项的 "timeLeft" 作为 prop 传递,使其成为一个哑组件。

import { useSnapshot } from 'valtio'
import { formatTimeDelta, calcTimeDelta } from './utils'
import { store } from './App'

export const Countdown = ({ index }: { index: number }) => {
  const snap = useSnapshot(store.todos[index])
  const delta = calcTimeDelta(snap.timeLeft)
  const { days, hours, minutes, seconds } = formatTimeDelta(delta)
  return (
    <span className="countdown-time">
      {delta.total < 0 ? '-' : ''}
      {days}
      {days ? ':' : ''}
      {hours}:{minutes}:{seconds}
    </span>
  )
}

在模块作用域中改变

让我们通过定义模块作用域中的递归 countdown 函数来管理多个计时器,而不是在 React 组件内部管理多个计时器,该函数将改变待办事项。

const countdown = (index: number) => {
  const todo = store.todos[index]
  // 用户删除待办事项的情况
  if (!todo) return
  // 待办事项完成或逾期的情况
  if (todo.status !== 'pending') {
    return
  }
  // 时间到了
  if (todo.timeLeft < 1000) {
    todo.timeLeft = 0
    todo.status = 'overdue'
    return
  }
  setTimeout(() => {
    todo.timeLeft -= 1000
    countdown(index)
  }, 1000)
}

我们可以从增强的 addTodo action 开始递归倒计时。

const addTodo = (e: React.SyntheticEvent, reset: VoidFunction) => {
  e.preventDefault()
  const target = e.target as typeof e.target & {
    deadline: { value: Date }
    description: { value: string }
  }
  const deadline = target.deadline.value
  const description = target.description.value
  const now = Date.now()
  store.todos.push({
    description,
    status: 'pending',
    id: now,
    timeLeft: new Date(deadline).getTime() - now,
  })
  // 清除表单
  reset()
  countdown(store.todos.length - 1)
}

请查看下面演示中的其余更改。

在模块作用域中订阅

能够在组件外部改变状态是一个巨大的好处。我们还可以在模块作用域中 subscribe 状态变化。我们将让您自己尝试这个。例如,您可能会将待办事项持久化到本地存储,如此示例中所示。

Codesandbox 演示