Jotai:终极 React 状态管理
为什么选择 Jotai?
与 Redux 的意识形态差异
Jotai 入门
基本语法
Jotai 实践:主题切换器挂钩
使用 atomWithStorage 重写钩子
关于Jotai的更多信息
衍生原子
只读原子
可读和可写原子
异步原子
Utils 的最佳作品
原子存储
原子重置
选择原子
应用于对象
冻结原子
等待所有
结论
Jotai 是一个相对较新的 React 状态管理库。它很简单,但毫无疑问,它是一个强大的库。
Jotai 基于 Facebook 新的Recoil模式和库。五年多以前,Facebook 创建了一个名为Flux的 React 状态管理模式和库。
基于这种模式,一些非 Facebook 开发者创建了自己的新库,它更加健壮、更易于使用,并席卷了 React 世界。这个库就是Redux。现在 Facebook 推出了 Recoil,它的理念与 Flux 不同。
Jotai 和 Redux 也是如此。让我们来探讨一下其中的一些。
为什么选择 Jotai?
- 简约的 API - Jotai 具有简单的 API 设计,使用起来非常愉快。
- 极小的包大小 - Jotai 的占用空间非常小,不会影响您的网站/应用程序的性能
- 满载而归 - Jotai 配备了许多
- 性能卓越 - Jotai 速度超快。它的运行时性能简直令人难以置信!
- TYPESCRIPT!!🥳🥳 - 一流的 TypeScript 支持!!预装了 Typings,TypeScript 创作体验超乎想象。
与 Redux 的意识形态差异
Jotai 几乎在所有方面都与 Redux 和React Context API截然不同。但有一个核心概念至关重要,你需要将其内化。
Redux 存储是单体的,但 Jotai 是原子的。
这意味着,在 Redux 中,应用会将所需的所有全局状态存储在一个大对象中。而在 Jotai 中,情况则相反。你需要将状态拆分成多个原子,例如,一个 store 对应一个单独的 store,或者多个紧密相关的状态。
Jotai 入门
安装 Jotai
# pnpm
pnpm add jotai
# npm
npm install jotai
# Or if you're a yarn person
yarn add jotai
在应用程序中进行设置
Jotai 要求提供程序必须存在于当前组件的父级中。最简单的方法是将整个应用包装在提供程序中,如下所示 👇
// index.jsx (or index.tsx)
import React from 'react';
import ReactDOM from 'react-dom';
import { App } from './App';
// Jotai provider
import { Provider } from 'jotai';
ReactDOM.render(
<React.StrictMode>
<Provider>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root'),
);
现在您可以在应用程序的任何地方使用 jotai!
基本语法
现在我们的基本设置已经完成,让我们来看看语法!
创建你的第一个原子
原子是宇宙的基石,聚集成分子——
不,不是那个原子😅。
Jotai 原子是一些独立的小状态块。理想情况下,一个原子包含的数据量非常小(尽管这只是一个惯例。你仍然可以把所有状态都放在一个原子中,尽管这会降低性能)。
以下是创建第一个原子的方法
import { atom } from 'jotai';
const themeAtom = atom('light');
就这样!你拥有了你的第一个状态!
注意,我在原子名称后加了
Atom
,例如themeAtom
。这不是规则或官方惯例。我只是为了在大型项目中清晰起见,才选择这样命名原子。你可以直接这样命名,theme
而不是themeAtom
🙂
那么,如何使用它呢?嗯,它的用法是结合使用useState
和useContext
hooks。
import { useAtom } from 'jotai';
export const ThemeSwitcher = () => {
const [theme, setTheme] = useAtom(themeAtom);
return <main>{theme}</main>;
};
看到了吗?和 useState 一模一样,但唯一的区别是我们创建的原子会被传递给 useState。useAtom 返回一个大小为 2 的数组,其中第一个元素是一个值,第二个元素是一个函数,用于设置原子的值。这使得所有依赖此原子的组件都会更新并重新渲染。
所以,如果我们把它们放在一起,完整的代码看起来就像这样👇
import { atom, useAtom } from 'jotai';
const themeAtom = atom('light');
export const ThemeSwitcher = () => {
const [theme, setTheme] = useAtom(themeAtom);
return <main>{theme}</main>;
};
注意,setTheme 函数还没有被使用。我们来修改一下 👇
import { atom, useAtom } from 'jotai';
const themeAtom = atom('light');
export const ThemeSwitcher = () => {
const [theme, setTheme] = useAtom(themeAtom);
return (
<main>
<p>Theme is {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle Theme</button>
</main>
);
};
朋友,这仅仅是个开始。Jotai 能做的远不止这些!
但仅凭这一点,我们无法给出太多的视角。切换值的按钮有什么特别之处呢?我同意。这个例子很无聊。让我们用 Jotai 做一个真正的主题切换器。
Jotai 实践:主题切换器挂钩
如今,每个应用程序、网站,甚至博客网站(尤其是博客)都需要主题切换。而制作一个主题切换器可能相当困难。首先,你必须设置 CSS 变量。然后,你必须选择一个主题,并创建一个切换主题的按钮。接下来,你必须确保使用 localstorage API 记住首选项。但这会让你在页面加载时获取正确的值,并且不会干扰服务端渲染 (SSR) 和预渲染等等……
是的,这确实很复杂。任何开发者在尝试之前都会感到畏惧(我就是这样🥶)。
所以,这才是最好的做法,让我们用 Jotai 来做吧。你会惊讶于 Jotai 竟然能把事情做得如此简单。
因此,我们的目标如下:
- 在服务器端工作(如不引用没有保护的文档或窗口)。
- 在本地存储中获取本地存储的值。
- 如果没有本地值,则尝试获取设备偏好,即设备主题是亮还是暗。
- 当前主题应可作为重新呈现其正在使用的组件的状态。
- 改变状态应该
localstorage
相应更新。
现在我们的列表已经完成,让我们看看代码👇
import { atom, useAtom } from 'jotai';
import { useEffect } from 'react';
const browser = typeof window !== 'undefined';
const localValue = browser ? localStorage.getItem('theme') : 'light';
const systemTheme =
browser && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
// The atom to hold the value goes here
const themeAtom = atom(localValue || systemTheme);
/** Sitewide theme */
export function useTheme() {
const [theme, setTheme] = useAtom(themeAtom);
useEffect(() => {
if (!browser) return;
localStorage.setItem('theme', theme);
document.body.classList.remove('light', 'dark');
document.body.classList.add(theme);
}, [theme]);
return [theme, setTheme];
}
这里发生了很多事。以下是详细内容。
我们检查当前代码是否正在浏览器中运行。如果我们在 SSR 或预渲染中运行代码,则此值为 false。
我们获取存储在 localstorage 中的值。如果 localstorage 中包含主题,我们会将其视为最高优先级,因为它是用户选择的。此外,由于 Node.js 中没有 localstorage,因此如果 Node.js 在 SSR 模式下运行,我们必须回退到默认值 light。
如果 localstorage 值不存在,我们还会使用 prefers-color-scheme: dark 来检索设备偏好设置。同样,如果设备偏好设置是 dark 或代码在 SSR 中运行,则会回退到 light 值。
最后,创建原子。这将是我们的主要存储,用于实际存储当前主题,可用作状态并可更改。注意我们赋予它的值:localValue || systemTheme
。这些值的作用如下:
如果在 SSR/预渲染模式下运行,localValue = 'light'
则会systemTheme = 'light', localValue || systemTheme
显示为轻量级。因此,这里需要注意的是:在 SSR 模式下,您的应用将使用轻量级主题,因此如果您预渲染应用,最终将使用轻量级主题(以纯 HTML 形式呈现)。JavaScript 加载时,它将同步到最相关的主题。
为什么我不直接把localValue
和systemTheme
变量放在钩子里呢?原因是:如果我把它们放在钩子里,每次在任何组件中初始化钩子,或者组件重新渲染时,这个钩子都会再次运行,并再次从 localstorage 和媒体查询中获取这些值。这些操作速度很快,但 localstorage 会阻塞,如果使用频繁,可能会导致卡顿。所以我们在应用的生命周期内只初始化这两个变量一次,因为我们只需要它们来获取初始值。
最后,让我们开始我们的钩子:
让我们使用 useAtom: 将此原子设置为局部状态const [theme, setTheme] = useAtom(themeAtom);
。这些将成为我们以状态形式呈现的主题。主题可以使用 进行修改setTheme
。
接下来,我们得到了钩子最重要的部分,它实际上会让当前主题为我们的 CSS 所知。
useEffect(() => {
if (!browser) return;
localStorage.setItem('theme', theme);
document.body.classList.remove('light', 'dark');
document.body.classList.add(theme);
}, [theme]);
每当主题更改时,它useEffect
都会运行,正如您在第二个参数中的数组中看到的那样。运行时,它会检查代码是否正在浏览器中运行。如果没有,它会通过返回来停止进一步的执行。
如果成功,则继续删除所有与放置主题相对应的类<body>
,然后添加与主题变量的最新值相对应的类。
最后,我们[theme, setTheme]
按原样返回该对,以便像使用 一样使用它useState
。您也可以将它们作为对象返回,{ theme, setTheme }
并为其提供明确的命名。
这就是这个钩子。
而且我的 TypeScript 亲戚也得到了保障 😉👇
import { atom, useAtom } from 'jotai';
import { useEffect } from 'react';
export type Theme = 'light' | 'dark';
const browser = typeof window !== 'undefined';
const localValue = (browser ? localStorage.getItem('theme') : 'light') as Theme;
const systemTheme: Theme =
browser && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
// The atom to hold the value goes here
const themeAtom = atom<Theme>(localValue || systemTheme);
/** Sitewide theme */
export function useTheme() {
const [theme, setTheme] = useAtom(themeAtom);
useEffect(() => {
if (!browser) return;
localStorage.setItem('theme', theme);
document.body.classList.remove('light', 'dark');
document.body.classList.add(theme);
}, [theme]);
return [theme, setTheme] as const;
}
这就是我们最终实现主题切换的稳定代码。由于 Jotai 的简洁性,这个钩子简单易懂(我希望如此😅)。
但问题是,与使用 Context API 相比,使用 Jotai 并没有节省多少代码。使用 Jotai 的代码应该也差不多这么简单,只是多了一点样板代码。所以这里其实没什么区别。
但是,这里有一个转折:我们可以通过使用 Jotai 提供的功能来摆脱更多的代码:atomWithStorage
我们可以将同步逻辑localstorage
完全移到钩子内部和外部。
使用 atomWithStorage 重写钩子
atomWithStorage
是一种特殊的原子,它会自动将提供给它的值与localstorage
或sessionStorage
(AsyncStorage
如果与 React Native 一起使用,则为 或 )同步,并在第一次加载时自动选择值!它在 jotai/utils 模块中可用,并且除了 Jotai Core 的 2.4KB 之外还增加了一些字节。
因此,我们可以这样重写它:
import { useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import { useEffect } from 'react';
const browser = typeof window !== 'undefined';
// The atom to hold the value goes here
const themeAtom = atomWithStorage(
'theme',
browser && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light',
);
/** Sitewide theme */
export function useTheme() {
const [theme, setTheme] = useAtom(themeAtom);
useEffect(() => {
if (!browser) return;
document.body.classList.remove('light', 'dark');
document.body.classList.add(theme);
}, [theme]);
return [theme, setTheme];
}
如你所见,我们完全摆脱了localstorage
代码中的 ,并获得了一个新东西atomWithStorage
。第一个参数是将其存储在 中的键localstorage
。例如,如果你theme
在这里指定了 值,你将使用 从 localstorage 中检索它localstorage.getItem('theme')
。
如你所见,代码本身在代码行数方面并没有减少多少。它只减少了 20%,对于这个已经很小的文件来说,这个数字并不算大。关键在于,我们通过 隐藏了复杂性atomWithStorage
。现在我们不必再考虑本地值的存储,只需专注于我们的主要逻辑,并记住这个值是在本地同步的,仅此而已。
使用这个钩子最终非常简单,
import { useTheme } from './use-theme';
export const ThemeSwitcher = () => {
const [theme, setTheme] = useTheme();
return (
<main>
<p>Theme is {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle Theme</button>
</main>
);
};
而且它真的有效!!🪄
关于Jotai的更多信息
这就是 Jotai 的基本介绍。我添加了 atomWithStorage 实用函数,以展示它如何让你的代码变得强大而简洁。稍后我会详细介绍这些实用函数。现在,让我们进一步探索基本的 atom 和 useAtom,以及它们如何赋予你超能力。
衍生原子
有时,你会想让一个原子依赖于另一个原子,也就是说,你想将多个原子组合成一个大的计算原子。使用 Jotai 可以非常简单地实现这一点。
只读原子
只读原子是依赖于其他原子的派生原子,我们不能直接改变它们的值。
例如,这些原子的用法如下👇
const [derivedValue] = useAtom(derivedAtom);
这里没有setDerivedValue
setter 函数。我们只能读取这个原子。更改它所属的原子会自动更新这个值。
好了,说得够多了!现在让我们看看如何创建这些派生原子。
到目前为止你已经看到了这个原子👇
const store = atom('someValue');
但你猜怎么着?原子可以接受函数作为参数👇
const store = atom((get) => get(someAtomDefinedSomewhere));
这里,我们传递的不是原始值,而是回调函数。这个回调函数有一个 get 参数,允许你访问其他原子的原始值。有了它,你可以做任何事情。乘法、连接、映射、归约等等,无所不能。
你可以用它做更多的事情。例如,一个简单的例子是将一个对象中所有符合特定条件的键列表放入一个数组中。
这是对象
export const appsStateStore = atom({
finder: false,
launchpad: false,
safari: false,
messages: false,
mail: true,
maps: true,
photos: false,
facetime: true,
calendar: false,
});
定义将打开的应用程序保存在数组中的原子👇
const openAppsStore = atom((get) => {
const apps = get(openAppsStore); // Gives the raw value { finder: false, launchpad: false, ...
// Filter out the values who are marked as false
const openAppsList = Object.keys(apps).filter((appName) => apps[appName]);
return openAppsList;
});
就是这样!当你调整 中的值appStateStore
,将它们设置为 true 和 false 时,openAppsStore
将反映更改,并且使用此存储的组件也将使用新值进行更新。
你也可以将许多不同的原子组合在一起👇
const xCoordinateAtom = atom(0);
const yCoordinateAtom = atom(0);
// Compose 'em all
const distanceFromOriginAtom = atom((get) =>
Math.sqrt(get(xCoordinateAtom) ** 2 + get(yCoordinateAtom) ** 2),
);
您可以调整xCoordinateAtom
原子和yCoordinateAtom
,然后它将distanceFromOriginAtom
使用新值进行更新!!)
这是一个计算点到原点 (0, 0) 距离的数学公式。如果你没看懂,不用担心,我只是想让你明白,不同的原子可以无缝地组合在一起。就是这样!🙂
可读和可写原子
这些原子源自其他原子,但也可以由用户自行修改。
const readWriteAtom = atom(
(get) => get(priceAtom) * 2,
(get, set, newPrice) => {
set(priceAtom, newPrice / 2);
// you can set as many atoms as you want at the same time
},
);
当你设置这个原子的值时,它会触发我们提供的自定义 write 函数,并可以修改它所依赖的原子。这基本上是双向数据绑定。你更改priceAtom
,它readWriteAtom
也会更新。你更新readWriteAtom
,priceAtom
它也会更新。是不是很棒?
但请注意:虽然这看起来很神奇,但它是双向数据绑定。过去对此存在争议,这是理所当然的,因为使用这种绑定方式,调试和保持数据流的合理性变得极其困难。这就是为什么 React 本身只有单向数据绑定。所以请谨慎使用这个原子。
异步原子
从这一点开始,我们进入了一个非常危险的领域:异步渲染,又名 React Suspense。
有时你的原子必须是异步的,也就是说,它们不是立即获取值,而是使用获取从远程源提取,这时你必须暂停渲染并等待数据返回。
这是使用异步原子的一个小代码演示👇
const fetchCountAtom = atom(
(get) => get(countAtom),
async (_get, set, url) => {
const response = await fetch(url);
set(countAtom, (await response.json()).count);
},
);
function Controls() {
const [count, compute] = useAtom(fetchCountAtom);
return <button onClick={() => compute('http://count.host.com')}>compute</button>;
}
但是如果你没有将控件包装在 Suspense 中,上述方法将不起作用👇
<Suspense fallback={<span />}>
<Controls />
</Suspense>
异步原子在构建现实世界的应用程序中非常有用,因为这些应用程序大多是添加了数据提取的 CRUD 应用程序。
Utils 的最佳作品
如果您喜欢 atomWithStorage 并且对它可以解锁的所有可能性感到兴奋,我为您准备了更多很棒的 Jotai 实用程序。
原子存储
我在文章一开始就提到了这一点,当时我重构了这个useTheme
钩子来使用这个特殊的原子。它接受一个键(即存储它的名称localstorage
)和一个初始值。然后你更改这个原子,它的值将被本地持久化,并在页面重新加载后获取。
import { atomWithStorage } from 'jotai/utils';
const darkModeAtom = atomWithStorage('darkMode', false);
这个原子也是 SSR 友好的,因此您可以毫无问题地对您的应用程序进行 SSR。
这个原子也可以存储值sessionStorage
,因此原子的值将一直保留到浏览器关闭。如果你正在构建一个银行 Web 应用,并且希望会话时间短,那么这个功能就非常方便了。
它也适用于 React Native,所以它几乎是通用的🤩
原子重置
有时你需要将状态重置为原始状态。传统的重置方法是将初始值存储在一个变量中,然后创建一个以该变量为值的状态,并在需要时setState
恢复到初始值。代码如下 👇
import { atom, useAtom } from 'jotai';
const initialValue = 'light';
const themeAtom = atom(initialValue);
function ThemeSwitcher() {
const [theme, setTheme] = useAtom(themeAtom);
const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light');
const resetTheme = () => setTheme(initialValue);
return (
<>
<button onClick={toggleTheme}>Toggle theme</button>
<button onClick={resetTheme}>Reset theme</button>
</>
);
}
这相当简单,但这里有一种更像 Jotai 的方式来做同样的事情👇
import { useAtom } from 'jotai';
import { atomWithReset, useResetAtom } from 'jotai/utils';
const themeAtom = atomWithReset('light');
function ThemeSwitcher() {
const [theme, setTheme] = useAtom(themeAtom);
const reset = useResetAtom(themeAtom);
const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light');
return (
<>
<button onClick={toggleTheme}>Toggle theme</button>
<button onClick={reset}>Reset theme</button>
</>
);
}
如你所见,我们稍微简化了组件。在本例中,简化的并不多,因为这是一个非常简单的示例。但我个人在我的应用中使用过这个重置原子,它包含一些基于复杂逻辑的组件,它确实让代码更加合理、符合语言习惯,并且没有 bug。
选择原子
如果有一个衡量库和框架酷炫程度的指标,那么 Jotai 就能凭借这个小工具打破它。
假设你有一个大物体。
const defaultPerson = {
name: {
first: 'Jane',
last: 'Doe',
},
birth: {
year: 2000,
month: 'Jan',
day: 1,
time: {
hour: 1,
minute: 1,
},
},
};
// Original atom.
const personAtom = atom(defaultPerson);
也就是说,许多组件依赖于这个特定的原子,但只需要其中的一部分。
问题是,当你更新这个原子时,所有依赖它的组件都会重新渲染。即使你只更改了birth.time.minute
,整个更新也会被视为一次更新,所有组件都会重新渲染。不幸的是,这就是 React 的工作原理。
但不用担心,因为 Jotai 也对此有解决方案!selectAtom
允许您仅使用整个对象的子路径来创建派生原子。
const firstNameAtom = selectAtom(personAtom, (person) => person.name.first);
firstNameAtom
是一个只读的派生原子,只有当person.name.first
属性改变时才会触发,它保存着 person.name.first 的值。
你可以更新该birth.time.hour
字段(即用新值更新整个原子),而依赖的组件firstNameAtom
将保持不变。很神奇吧?
应用于对象
问题出现了:如果你监听一个对象类型的字段,例如 person.birth,那么这个原子的效率会很低。Jotai 使用相等性检查 (===) 来检查原子的部分是否发生变化,以及是否需要重新渲染。问题是,没有两个对象是完全相同的。 === 检查的是对象的引用,而不是值。所以,基本上,在这个场景下,这个原子几乎没什么用。但也不完全是!
您可以为 this 提供第三个参数selectAtom
,这是您自己的相等性检查版本。您可以编写自定义函数来检查对象。
const birthAtom = selectAtom(personAtom, (person) => person.birth, deepEqual);
OFC,自己编写deepEqual
很难,因此建议使用 lodash-es 的isEqual
功能。
import { isEqual } from 'lodash-es';
const birthAtom = selectAtom(personAtom, (person) => person.birth, isEqual);
如果你看到 lodash 就担心打包文件大小,我向你保证,lodash-es 的 isEqual 是可摇树的,压缩后只有 4.4KB,使用 gzip/brotli 压缩后甚至更小。所以不用担心 😁
这可以让你的应用性能从零提升到极致。真的!
冻结原子
import { atom } from 'jotai';
import { freezeAtom } from 'jotai/utils';
const objAtom = freezeAtom(atom({ count: 0 }));
freezeAtom
接受一个现有原子并返回一个新的派生原子。返回的原子是“冻结的”,这意味着当你useAtom
在组件中使用该原子或传入其他原子时,原子值将被深度冻结Object.freeze
。这有助于查找你意外尝试改变对象而导致意外行为的错误。
这个原子主要是为了方便调试,比如当你改变对象状态时(在 React 中你不应该这样做,不过,毕竟我们都是人)。这种情况很常见,我很高兴 Jotai 的团队提供了如此高质量的调试工具。
等待所有
还记得上面关于异步原子的部分吗?这个实用程序就是为此而设计的,而且非常方便。
const dogsAtom = atom(async (get) => {
const response = await fetch('/dogs');
return await response.json();
});
const catsAtom = atom(async (get) => {
const response = await fetch('/cats');
return await response.json();
});
const App = () => {
const [dogs] = useAtom(dogsAtom);
const [cats] = useAtom(catsAtom);
// ...
};
现在你已经有了这两个异步原子,并且在应用中使用了它们。一切正常。但是这里有一个小问题:组件会等待第一个原子dogsAtom
去获取数据并返回,然后才会转到下一个原子catsAtom
。我们不希望出现这种情况。这两个原子彼此独立,我们应该并行获取它们(或者,如果你是一个 JavaScript 硬核开发者,也可以同时获取它们😉)。
所以,我们基本上想Promise.all(...)
对这些原子执行类似 await 的操作。实现方法是使用waitForAll
util。
使用后,我们的代码变为👇
const dogsAtom = atom(async (get) => {
const response = await fetch('/dogs');
return await response.json();
});
const catsAtom = atom(async (get) => {
const response = await fetch('/cats');
return await response.json();
});
const App = () => {
const [[dogs, cats]] = useAtom(waitForAll([dogsAtom, catsAtom]));
// ...
};
现在它等待它们都解析完毕,然后返回一个包含两者返回数据的数组。有点像一个await Promise.all
语句。
从字面上看,此时,React 应该将 Jotai 吸收到自身中,这太好了!!
这些只是 Jotai 提供的所有实用程序的一半。Jotai 提供的功能非常丰富,我可以写一本书来介绍它。前往Jotai 文档了解更多。
Jotai 和它的亲戚相处得很好🤝
Jotai 与其他图书馆完全不同,它们就像:“你的心中只有我package.json
!!!”
不,Jotai 不是那样运作的!Jotai 本身是一个很棒的状态管理库,但它也允许你与其他状态管理库无缝集成。
以下是 Jotai 附带的所有官方集成:
- 伊默尔
- 光学
- 反应查询
- 状态
- 瓦尔蒂奥
- 立场
- Redux
- 尿质问卷
现在,这篇博文已经太长了,无法涵盖上述集成,但我想介绍一下 Immer。为什么?因为 React 状态最大的痛点:不可变性。
不变性很棒,它让你更容易理解 React State,但当你的状态是一个对象时,事情就会变得非常困难。你必须处理扩展对象并与要更新的属性合并的整个过程。
function UpdateUser() {
const [user, setUser] = useState({
id: 23,
name: 'Luke Skywalker',
dob: new Date('25 December, 19 BBY'),
});
// Update the dob
const updateDob = () => setUser({ ...user, dob: new Date('25 November, 200ABY') });
return <button onClick={updateDob}>Update DOB</button>;
}
正如您在方法中看到的updateDob
,我们必须展开原始对象,并传递要更新的字段。这没问题。但是,如果对象深度很多,而我们想要更新一个非常深的对象,该怎么办呢?
它变得如此复杂,我个人从未尝试过。我只是重新架构了我的状态,使其在某种程度上变得更浅,然后进行更新。我更喜欢 Svelte 而不是 React,在 Svelte 中,你只需简单地修改状态就可以了。
user.dob = new Date('25 November, 200ABY');
并且它也极其深奥!
state.depth1.depth2.depth3.depth4 = 'something';
因此,React 所需的一切对我来说总是感觉不对。
但这就是 Immer 的用武之地。Immer 允许你直接修改状态,而且它真的有效。你可以自己去看看。
import { atomWithImmer } from 'jotai/immer';
const userAtom = atomWithImmer({
id: 23,
name: 'Luke Skywalker',
dob: new Date('25 December, 19 BBY'),
});
function UpdateUser() {
const [user, setUser] = useAtom(userAtom);
// Update the dob
const updateDob = () =>
setUser((user) => {
user.dob = new Date('25 November, 200ABY');
return user;
});
return <button onClick={updateDob}>Update DOB</button>;
}
这里的setUser
工作方式有所不同。它是一个回调函数,会将状态的当前值传递给你。这个值是原始值的副本。你可以在回调函数中随意修改这个副本,最后只需返回它即可。Jotai 和 Immer 会自动协调这些更改,而不会出现任何修改带来的 bug。真是太棒了!
结论
好吧!!这篇文章好长啊!恭喜你坚持读到最后(略读也算😉)。
本文只是对 Jotai 的简单介绍。Jotai文档中还有更多内容,您一定要去看看。
平安✌️
文章来源:https://dev.to/puruvj/jotai-the-ultimate-react-state-management-5cil