use
hook。 现在,您可以在 React 之外使用 store interface 了。Jotai 采用 Atom 风格的方式进行全局的 React 状态管理。
通过组合 atom 来构建状态,并根据 atom 的依赖关系自动优化渲染。 这解决了 React 上下文的额外重新渲染问题,消除了 对 memoization 的需求,并在保持声明式编程模型的同时, 提供了与信号(signals)类似的开发体验。
它可以从简单的 useState
替代品扩展到 具有复杂需求的企业级 TypeScript 应用程序。此外,还有大量实用工具和 扩展程序可以为你提供帮助!
Jotai 已被应用于这些创新型公司的生产环境中。
接下来将指导您创建一个简单的 Jotai 应用程序。首先是 安装 Jotai,然后探索核心 API 的基础知识,最后在应用于 React 框架中进行服务器端渲染。
安装
首先将 Jotai 作为依赖项添加到 React 项目中。
# npmnpm i jotai# yarnyarn add jotai# pnpmpnpm add jotai
创建 atom
首先创建原始和派生类型的 atom 来构建状态。
原始类型的 atom
原始类型的 atom 可以是后面列出的任何类型:booleans、numbers、strings、objects、arrays、sets、maps、 等。
import { atom } from 'jotai'const countAtom = atom(0)const countryAtom = atom('Japan')const citiesAtom = atom(['Tokyo', 'Kyoto', 'Osaka'])const animeAtom = atom([{title: 'Ghost in the Shell',year: 1995,watched: true},{title: 'Serial Experiments Lain',year: 1998,watched: false}])
派生类型的 atom
派生类型的 atom 在返回自身的值之前,可以读取其它 atom 的值。
const progressAtom = atom((get) => {const anime = get(animeAtom)return anime.filter((item) => item.watched).length / anime.length})
使用 atom
在 React 组件中使用 atom 来读取或写入状态。
从同一个组件中读取和写入
在同一个组件中读取和写入 atom 时,为简便起见,请使用合并后的 useAtom
hook。
import { useAtom } from 'jotai'const AnimeApp = () => {const [anime, setAnime] = useAtom(animeAtom)return (<><ul>{anime.map((item) => (<li key={item.title}>{item.title}</li>))}</ul><button onClick={() => {setAnime((anime) => [...anime,{title: 'Cowboy Bebop',year: 1998,watched: false}])}}>Add Cowboy Bebop</button><>)}
从不同的组件中读取和写入
当 atom 的值为只允许读或写时,请使用 useAtomValue
和 useSetAtom
这两个 hook 来 优化重新渲染的效率。
import { useAtomValue, useSetAtom } from 'jotai'const AnimeList = () => {const anime = useAtomValue(animeAtom)return (<ul>{anime.map((item) => (<li key={item.title}>{item.title}</li>))}</ul>)}const AddAnime = () => {const setAnime = useSetAtom(animeAtom)return (<button onClick={() => {setAnime((anime) => [...anime,{title: 'Cowboy Bebop',year: 1998,watched: false}])}}>Add Cowboy Bebop</button>)}const ProgressTracker = () => {const progress = useAtomValue(progressAtom)return (<div>{Math.trunc(progress * 100)}% watched</div>)}const AnimeApp = () => {return (<><AnimeList /><AddAnime /><ProgressTracker /></>)}
服务器端渲染
如果服务器端渲染使用的是类似 Next.js 或 Waku 之类的框架时,请确保在 root 节点下 添加一个 Jotai Provider。
Next.js (使用 app 目录)
在单独的客户端组件中创建 Jotai provider。然后将新建的 provider 导入(import)到根目录下的 layout.js
服务器端组件中。
// ./components/providers.js'use client'import { Provider } from 'jotai'export const Providers = ({ children }) => {return (<Provider>{children}</Provider>)}// ./app/layout.jsimport { Providers } from '../components/providers'export default function RootLayout({ children }) {return (<html lang="en"><body><Providers>{children}</Providers></body></html>)}
Next.js (使用 pages 目录)
在 _app.js
中添加 Jotai provider。
// ./pages/_app.jsimport { Provider } from 'jotai'export default function App({ Component, pageProps }) {return (<Provider><Component {...pageProps} /></Provider>)}
Waku
在单独的客户端组件中创建 Jotai provider。然后将新建的 provider 导入(import)到根 布局文件中。
// ./src/components/providers.js'use client'import { Provider } from 'jotai'export const Providers = ({ children }) => {return (<Provider>{children}</Provider>)}// ./src/pages/_layout.jsimport { Providers } from '../components/providers'export default async function RootLayout({ children }) {return (<Providers>{children}</Providers>)}
核心
Jotai 的 API 很精简,并且是面向 TypeScript 的。它与 React 内置的 useState
hook 一样简单易用,并且所有状态都是全局可访问的, 派生状态也易于实现,并且自动消除了不必要的重新渲染。
import { atom, useAtom } from 'jotai'// Create your atoms and derivativesconst textAtom = atom('hello')const uppercaseAtom = atom((get) => get(textAtom).toUpperCase())// Use them anywhere in your appconst Input = () => {const [text, setText] = useAtom(textAtom)const handleChange = (e) => setText(e.target.value)return (<input value={text} onChange={handleChange} />)}const Uppercase = () => {const [uppercase] = useAtom(uppercaseAtom)return (<div>Uppercase: {uppercase}</div>)}// Now you have the componentsconst App = () => {return (<><Input /><Uppercase /></>)}
实用工具
Jotai 软件包还包含了一个 jotai/utils
包。这些额外的 函数提供了在 localStorage 中持久化 atom、在服务器端渲染时hydrating an atom、 使用类似 Redux 的 reducers 和 action types 创建 atom 等功能的 支持。
import { useAtom } from 'jotai'import { atomWithStorage } from 'jotai/utils'// Set the string key and the initial valueconst darkModeAtom = atomWithStorage('darkMode', false)const Page = () => {// Consume persisted state like any other atomconst [darkMode, setDarkMode] = useAtom(darkModeAtom)const toggleDarkMode = () => setDarkMode(!darkMode)return (<><h1>Welcome to {darkMode ? 'dark' : 'light'} mode!</h1><button onClick={toggleDarkMode}>toggle theme</button></>)}
扩展
每个官方扩展都是独立的软件包:tRPC、Immer、Query、XState、 URQL、Optics、Relay、location、molecules、cache 等。
某些扩展提供了新的 atom 类型并自带写入函数,例如 atomWithImmer
(Immer) 或 atomWithMachine
(XState)。
其它新的 atom 类型提供了双向数据绑定函数,例如 atomWithLocation
或 atomWithHash
。
import { useAtom } from 'jotai'import { atomWithImmer } from 'jotai-immer'// Create a new atom with an immer-based write functionconst countAtom = atomWithImmer(0)const Counter = () => {const [count] = useAtom(countAtom)return (<div>count: {count}</div>)}const Controls = () => {// setCount === update: (draft: Draft<Value>) => voidconst [, setCount] = useAtom(countAtom)const increment = () => setCount((c) => (c = c + 1))return (<button onClick={increment}>+1</button>)}