JotaiJotai

Jotai
React 原始而灵活的状态管理

Effect

jotai-effect 是一个用于响应式副作用的实用程序包。

安装

npm i jotai-effect

atomEffect

atomEffect 是一个用于声明副作用和同步 Jotai 原子的实用函数。它对于观察和响应状态变化非常有用。

参数

type CleanupFn = () => void
type EffectFn = (
get: Getter & { peek: Getter },
set: Setter & { recurse: Setter },
) => CleanupFn | void
declare function atomEffect(effectFn: EffectFn): Atom<void>

effectFn (必需): 一个用于通过 get 监听状态更新和通过 set 写入状态更新的函数。effectFn 对于创建与其他 Jotai 原子交互的副作用非常有用。您可以通过返回一个清理函数来清理这些副作用。

使用

订阅原子变化

import { atomEffect } from 'jotai-effect'
const loggingEffect = atomEffect((get, set) => {
// 在挂载或某个原子发生变化时运行
const value = get(someAtom)
loggingService.setValue(value)
})

设置和拆解副作用

import { atomEffect } from 'jotai-effect'
const subscriptionEffect = atomEffect((get, set) => {
const unsubscribe = subscribe((value) => {
set(valueAtom, value)
})
return unsubscribe
})

使用原子或钩子挂载

在使用 atomEffect 定义了一个效果后,它可以被集成到另一个原子的读取函数中,或者传递给 Jotai 钩子。

const anAtom = atom((get) => {
// 当 anAtom 挂载时,挂载 atomEffect
get(loggingEffect)
// ...
})
// 当组件挂载时,挂载 atomEffect
function MyComponent() {
useAtom(subscriptionEffect)
// ...
}

atomEffect 的行为

  • 清理函数: 在卸载或重新评估之前,会调用清理函数。

    示例
    atomEffect((get, set) => {
    const intervalId = setInterval(() => set(clockAtom, Date.now()))
    return () => clearInterval(intervalId)
    })
  • 抵抗无限循环:atomEffect 通过 set 改变它正在观察的值时,不会重新运行。

    示例
    const countAtom = atom(0)
    atomEffect((get, set) => {
    // 这不会无限循环
    get(countAtom) // 在挂载后,计数将为 1
    set(countAtom, increment)
    })
  • 支持递归: 对于同步和异步的使用场景,都支持使用 set.recurse 进行递归,但在清理函数中不支持。

    示例
    const countAtom = atom(0)
    atomEffect((get, set) => {
    // 每秒增加一次计数
    const count = get(countAtom)
    const timeoutId = setTimeout(() => {
    set.recurse(countAtom, increment)
    }, 1000)
    return () => clearTimeout(timeoutId)
    })
  • 支持 Peek: 使用 get.peek 读取原子数据,而不订阅更改。

    示例
    const countAtom = atom(0)
    atomEffect((get, set) => {
    // 当 countAtom 发生变化时,不会重新运行
    const count = get.peek(countAtom)
    })
  • 在下一个微任务中执行: effectFn 在下一个可用的微任务中运行,所有 Jotai 的同步读取评估完成后。

    示例
    const countAtom = atom(0)
    const logAtom = atom([])
    const logCounts = atomEffect((get, set) => {
    set(logAtom, (curr) => [...curr, get(countAtom)])
    })
    const setCountAndReadLog = atom(null, async (get, set) => {
    get(logAtom) // [0]
    set(countAtom, increment) // 效果在下一个微任务中运行
    get(logAtom) // [0]
    await Promise.resolve().then()
    get(logAtom) // [0, 1]
    })
    store.set(setCountAndReadLog)
  • 批量同步更新 (原子事务):atomEffect 原子依赖的多个同步更新被批量处理。效果是以最终值作为单个原子事务运行。

    示例
    const enabledAtom = atom(false)
    const countAtom = atom(0)
    const updateEnabledAndCount = atom(null, (get, set) => {
    set(enabledAtom, (value) => !value)
    set(countAtom, (value) => value + 1)
    })
    const combos = atom([])
    const combosEffect = atomEffect((get, set) => {
    set(combos, (arr) => [...arr, [get(enabledAtom), get(countAtom)]])
    })
    store.set(updateEnabledAndCount)
    store.get(combos) // [[false, 0], [true, 1]]
  • 有条件地运行 atomEffect: atomEffect 只在其在应用程序中被挂载时才处于活动状态。这可以防止在不需要时进行不必要的计算和副作用。你可以通过卸载它来禁用效果。

    示例
    atom((get) => {
    if (get(isEnabledAtom)) {
    get(effectAtom)
    }
    })
  • 幂等性: atomEffect 在状态改变时只运行一次,无论它被挂载了多少次。

    示例
    let i = 0
    const effectAtom = atomEffect(() => {
    get(countAtom)
    i++
    })
    const mountTwice = atom(() => {
    get(effectAtom)
    get(effectAtom)
    })
    store.set(countAtom, increment)
    Promise.resolve.then(() => {
    console.log(i) // 1
    })

依赖管理

除了挂载事件外,当任何依赖项的值改变时,效果也会运行。

  • 同步: 在效果的同步评估期间,使用 get 访问的所有原子都会被添加到原子的内部依赖图中。

    示例
    atomEffect((get, set) => {
    // 当 `anAtom` 的值改变时更新,但当 `anotherAtom` 的值改变时不更新
    get(anAtom)
    setTimeout(() => {
    get(anotherAtom)
    }, 5000)
    })
  • 异步: 对于异步效果,你应该使用一个中止控制器来取消待处理的获取请求和承诺。

    示例
    atomEffect((get, set) => {
    const count = get(countAtom) // countAtom 是一个原子依赖
    const abortController = new AbortController()
    (async () => {
    try {
    await delay(1000)
    abortController.signal.throwIfAborted()
    get(dataAtom) // dataAtom 不是一个原子依赖
    } catch (e) {
    if (e instanceof AbortError) {
    // 这里是异步清理逻辑
    } else {
    console.error(e)
    }
    }
    })()
    return () => {
    // 当 countAtom 改变时中止
    abortController.abort(new AbortError())
    }
    })
  • 清理: 在清理函数中使用 get 访问原子不会将它们添加到原子的内部依赖图中。

    示例
    atomEffect((get, set) => {
    // 在挂载时运行一次
    // 当 `idAtom` 改变时不更新
    const unsubscribe = subscribe((valueAtom) => {
    const value = get(valueAtom)
    // ...
    })
    return () => {
    const id = get(idAtom)
    unsubscribe(id)
    }
    })
  • 依赖图的重新计算: 每次运行时都会重新计算依赖图。如果在当前运行期间没有监视某个原子,那么它将不会在当前运行的依赖图中。只有被积极监视的原子才被视为依赖项。

    示例
    const isEnabledAtom = atom(true)
    atomEffect((get, set) => {
    // 如果 `isEnabledAtom` 为真,当 `isEnabledAtom` 或 `anAtom` 的值改变时运行
    // 否则,当 `isEnabledAtom` 或 `anotherAtom` 的值改变时运行
    if (get(isEnabledAtom)) {
    const aValue = get(anAtom)
    } else {
    const anotherValue = get(anotherAtom)
    }
    })

与 useEffect 的比较

组件副作用

useEffect 是一个 React Hook,它让你可以将组件与外部系统同步。

Hooks 是一种函数,让你可以从函数组件中“钩入” React 的状态和生命周期特性。 它们是一种复用,但不集中,有状态的逻辑。 每次调用 hook 都有一个完全独立的状态。 这种隔离可以被称为 组件范围的。 对于将组件 props 和状态与 Jotai 原子同步,你应该使用 useEffect hook。

全局副作用

对于设置全局副作用,选择 useEffect 和 atomEffect 取决于开发者的偏好。 你是更喜欢直接在组件中构建这个逻辑,还是在 Jotai 状态模型中构建这个逻辑,取决于你采用的思维模型。

atomEffects 更适合在原子中建模行为。 它们的范围是存储上下文,而不是组件。 这保证了无论有多少次调用,都只会使用一个效果。

如果你确保 useEffect 是幂等的,那么也可以通过 useEffect hook 实现同样的保证。

atomEffects 与 useEffect 的区别还在于其他几个方面。它们可以直接对原子状态的改变做出反应,对无限循环有抵抗力,并且可以有条件地挂载。

由你决定

useEffect 和 atomEffect 都有自己的优点和应用。你的项目的具体需求和你的舒适程度应该指导你的选择。 总是倾向于那种给你更顺畅、更直观的开发体验的方法。祝你编程愉快!