JotaiJotai

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

Storage

atomWithStorage

参考:https://github.com/pmndrs/jotai/pull/394

import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
const darkModeAtom = atomWithStorage('darkMode', false)
const Page = () => {
const [darkMode, setDarkMode] = useAtom(darkModeAtom)
return (
<>
<h1>Welcome to {darkMode ? 'dark' : 'light'} mode!</h1>
<button onClick={() => setDarkMode(!darkMode)}>toggle theme</button>
</>
)
}

atomWithStorage 函数创建了一个原子,其值在 React 的 localStoragesessionStorage,或者在 React Native 的 AsyncStorage 中持久化。

参数

key(必需):在将状态与localStorage,sessionStorage或AsyncStorage同步时使用的唯一字符串作为键

initialValue(必需):原子的初始值

storage(可选):具有以下方法的对象:

  • getItem(key, initialValue)(必需):从存储中读取项目,或回退到initialValue
  • setItem(key, value)(必需):将项目保存到存储
  • removeItem(key)(必需):从存储中删除项目
  • subscribe(key, callback, initialValue)(可选):订阅外部存储更新的方法。

options(可选):具有以下属性的对象:

  • getOnInit(可选,默认为false):一个布尔值,表示是否在初始化时从存储中获取项目。请注意,在SPA中,如果getOnInit未设置或为false,则始终会在初始化时获取初始值,而不是存储的值。如果首选存储的值,将getOnInit设置为true

如果未指定,默认的存储实现使用localStorage进行存储/检索,JSON.stringify()/JSON.parse()进行序列化/反序列化,并订阅storage事件进行跨标签同步。

createJSONStorage 工具

为了使用JSON.stringify()/JSON.parse()创建自定义存储实现,提供了createJSONStorage工具。

使用:

const storage = createJSONStorage(
// getStringStorage
() => localStorage, // 或 sessionStorage, asyncStorage 或类似的
// options (可选)
{
reviver, // JSON.parse 的可选 reviver 选项
replacer, // JSON.stringify 的可选 replacer 选项
},
)

注意:JSON.parse 不是类型安全的。如果它不能接受任何类型,那么对于生产应用程序,某种验证将是必要的。

服务器端渲染

任何依赖于存储原子值的JSX标记(例如,classNamestyle属性)在服务器上渲染时将使用initialValue(因为localStoragesessionStorage在服务器上不可用)。

这意味着,如果用户有一个与initialValue不同的storedValue,那么最初提供给用户浏览器的HTML和React在重新水合过程中期望的内容之间将存在不匹配。

对此问题的建议解决方案是只在客户端渲染依赖于storedValue的内容,通过将其包装在一个自定义的<ClientOnly>包装器中,该包装器只在重新水合后渲染。技术上可能有其他解决方案,但是需要在initialValue切换到storedValue时短暂的"闪烁",这可能会导致不愉快的用户体验,因此建议使用此解决方案。

从存储中删除项目

对于您想从存储中删除项目的情况, 使用atomWithStorage创建的原子在写入时接受RESET符号。

参见以下示例以了解使用方法:

import { useAtom } from 'jotai'
import { atomWithStorage, RESET } from 'jotai/utils'
const textAtom = atomWithStorage('text', 'hello')
const TextBox = () => {
const [text, setText] = useAtom(textAtom)
return (
<>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={() => setText(RESET)}>Reset (to 'hello')</button>
</>
)
}

如果需要,您还可以根据先前的值进行条件重置。

如果您希望在先前的值满足某个条件时清除localStorage中的键,这可能特别有用。

以下示例说明了这种用法,即每当先前的值为true时,清除visible键。

import { useAtom } from 'jotai'
import { atomWithStorage, RESET } from 'jotai/utils'
const isVisibleAtom = atomWithStorage('visible', false)
const TextBox = () => {
const [isVisible, setIsVisible] = useAtom(isVisibleAtom)
return (
<>
{ isVisible && <h1>Header is visible!</h1> }
<button onClick={() => setIsVisible((prev) => prev ? RESET : true))}>Toggle visible</button>
</>
)
}

React-Native 实现

你可以使用任何实现了 getItemsetItemremoveItem 的库。 假设你会使用社区提供的标准 AsyncStorage。

import { atomWithStorage, createJSONStorage } from 'jotai/utils'
import AsyncStorage from '@react-native-async-storage/async-storage'
const storage = createJSONStorage(() => AsyncStorage)
const content = {} // 任何可以 JSON 序列化的内容
const storedAtom = atomWithStorage('stored-key', content, storage)

使用 AsyncStorage 的注意事项(自 v2.2.0 起)

如果你使用任何类型的 AsyncStorage,原子值可能会变为异步。因此,如果你要更新值,你可能需要解析 promise。

const countAtom = atomWithStorage('count-key', 0, anyAsyncStorage)
const Component = () => {
const [count, setCount] = useAtom(countAtom)
const increment = () => {
setCount(async (promiseOrValue) => (await promiseOrValue) + 1)
}
// ...
}

验证存储的值

要为你的存储原子添加运行时验证,你需要创建一个自定义的存储实现。

下面是一个例子,它使用 Zod 来验证存储在 localStorage 中的值,并进行跨标签同步。

import { atomWithStorage } from 'jotai/utils'
import { z } from 'zod'
const myNumberSchema = z.number().int().nonnegative()
const storedNumberAtom = atomWithStorage('my-number', 0, {
getItem(key, initialValue) {
const storedValue = localStorage.getItem(key)
try {
return myNumberSchema.parse(JSON.parse(storedValue ?? ''))
} catch {
return initialValue
}
},
setItem(key, value) {
localStorage.setItem(key, JSON.stringify(value))
},
removeItem(key) {
localStorage.removeItem(key)
},
subscribe(key, callback, initialValue) {
if (
typeof window === 'undefined' ||
typeof window.addEventListener === 'undefined'
) {
return
}
window.addEventListener('storage', (e) => {
if (e.storageArea === localStorage && e.key === key) {
let newValue
try {
newValue = myNumberSchema.parse(JSON.parse(e.newValue ?? ''))
} catch {
newValue = initialValue
}
callback(newValue)
}
})
},
})

我们还有一个新的工具 unstable_withStorageValidator 来简化一些情况。 上述情况将变为:

import {
atomWithStorage,
createJSONStorage,
unstable_withStorageValidator as withStorageValidator,
} from 'jotai/utils'
import { z } from 'zod'
const myNumberSchema = z.number().int().nonnegative()
const isMyNumber = (v) => myNumberSchema.safeParse(v).success);
const storedNumberAtom = atomWithStorage(
'my-number',
0,
withStorageValidator(isMyNumber)(createJSONStorage()),
)