了解如何在现代 React 应用中管理状态,什么是远程状态、URL 状态、本地状态和共享状态,以及何时真正需要状态管理库。
原文链接:www.developerway.com/posts/react…
作者: Nadia Makarevich
若你长期关注我的博客,便知我向来避免撰写"观点"类文章。我的写作多聚焦于技术原理的阐释,其中本无容纳个人见解的余地。
但这次不同!时常有人询问我对状态管理、最佳库及实践方法的看法。这类话题自然难以回避主观判断。那么当下该选择何种库?是经典的Redux?还是新锐的Zustand?抑或另辟蹊径?干脆直接用Context如何?
在此抛砖引玉:其实你根本不需要任何库!若好奇缘由,欢迎继续阅读——不过我还是躲在桌子底下保险些。😅
你为何想要做出状态管理决策?
那么,究竟哪种状态管理方案最优?且慢——你为何想知道答案?不,我是认真的。
世上本无"最佳"之物,一切皆因情境而异。(冰淇淋除外,开心果口味永远是最佳选择)。
若你只是想学习新技能充实简历、提升求职竞争力,那就掌握最流行的库吧。《React现状报告》这类调研报告就是你的好帮手。更明智的做法是——打开目标公司的职位描述,直接提取所需技术名称。最明智的做法(更明智?🤔)是掌握React的核心与进阶概念,比如重渲染和状态协调机制。这样所有状态管理库在你眼中都大同小异,不过是用不同方式表达相同概念罢了。
如果你正在处理一个现有的老旧大型项目,对当前使用的Redux状态管理方案感到不满,并希望引入更优解,那么你其实也不需要"最完美"的库。此时最关键的是弄清楚当前库为何让你不满意。最不该做的事就是用差异巨大的方案替换成熟方案——这不仅需要投入大量精力培训同事,还可能无法解决你承诺要解决的问题。典型例子就是试图用XState之类方案替代Redux,以解决"Redux过于复杂"的问题。
你或许觉得现有库拖累性能,需要更优方案。但请先确认:库本身是否真有问题?你有多确定?请用数据说话。我敢打赌,库本身的影响微乎其微。更可能的情况是:问题出在未优化的重渲染代码,或是关键路径上的低效计算。这些问题或许受库的编程模式影响,也可能无关!这或许揭示了组织内部更深层的工程问题。无论如何,在未彻底理解问题根源前贸然更换方案,多半徒劳无功。
有趣的是,在排查当前代码问题时,你可能会发现根本不需要专门的状态管理库。在"Redux盛行时期",各类状态管理需求被混为一谈。但如今,许多场景更适合采用针对特定需求的专用库。因此对你当前用例而言,最佳方案可能是逐步用三种不同库替换旧库——此时状态管理问题几乎可以忽略不计。
若你正着手启动全新项目,摆脱任何技术遗留影响,并希望预先选定最佳技术栈(包括状态管理方案),以避免因技术更迭导致选用的库在一年后突然过时而被迫迁移——这种情况下,我钦佩你的远见卓识,本文后续内容正是为你而写。
无需状态管理库的状态关切事项
那么,我们想要管理的这个"状态"究竟是什么?
本质上,它就是数据。影响系统运作或行为的数据。例如当你吃冰淇淋时,作为系统的心理状态可以定义为:
- 期待吃冰淇淋的状态
- 享受吃冰淇淋的状态
- 吃完冰淇淋后的满足状态
或者聚焦于React框架时,状态可以是:
- 模态对话框的开启状态(打开/关闭)。
- 从接口获取数据的状态(无数据/加载中/加载失败/加载成功)。
- 组件生命周期的状态(已挂载/未挂载)。
或是任何可能影响屏幕渲染效果、或影响组件响应用户交互行为的数据。
说到数据和React...
远程状态
"状态管理"的一个非常典型且通常相当复杂的应用场景,始终是处理"远程"数据。即存在于外部(如数据库)的数据,我们需要通过REST接口(多数情况下)获取这些数据并集成到React应用中。
即便最简单的"获取并渲染"场景也并非表面那般简单。界面至少需要处理三种数据状态:数据尚未获取、数据正在加载、数据已成功获取。
但数据加载过程中可能发生意外,例如服务器故障。因此需要在此处添加异常处理机制。当多个组件需要从完全相同的接口获取数据时,也必须避免触发重复请求。
对了,若在某页面成功获取数据,通常需要缓存以供其他页面复用。不过还需设计机制定期刷新缓存避免数据过时,或直接彻底清除缓存。
说到缓存。你肯定想避免请求瀑布吧?当数据请求被困在条件渲染组件中,而该组件只有在另一个完全无关的请求完成后才会触发。这种情况下,你需要并行处理这些请求,预先获取隐藏数据存入缓存,并在需要时从缓存中提取。
顺便提一句,我仍在讨论"获取、渲染、遗忘"的典型场景!但若需响应用户交互来获取数据呢?此时竞态条件及其处理难题便会加入战局。
若数据可被修改,你将陷入痛苦境地——尤其当你试图在保持数据完整性的同时引入"乐观更新"机制时。
因此若要将旧应用从 Redux 迁移,你现有代码中约 80% 的 Redux 相关逻辑都在处理上述问题。
此时你需要的并非状态管理库,而是优秀的React优先数据管理库——它应能解决上述所有问题,且具备持续维护、良好声誉、完善文档及稳定性。具体而言,至少应满足:版本号大于1.x,且不会每半年进行重大版本重写。
对于这种用例,我的首选方案是 TanStack Query(原名 React Query)。早说了,这会是主观推荐而非"让我们深入探讨"的文章!但真心建议你尝试从基于 Redux 的旧式定制方案迁移过来——你会喜极而泣,开发体验将彻底改变,80% 的代码量将神奇消失。
想在组件中获取数据时同时关注加载和错误状态?很简单:
function Component() {
const { isPending, error, data } = useQuery({
queryKey: ['my-data'],
queryFn: () => fetch('https://my-url/data').then((res) => res.json()),
});
if (isPending) return 'Loading...';
if (error) return 'Oops, something went wrong';
return ... // render whatever here based on the data
}
想在另一个组件中调用此接口而不触发额外请求?别担心,只需使用相同的queryKey,库会自动处理。
想预取并缓存部分请求?无需修改现有代码,只需在需要触发预取的位置添加"queryClient.prefetchQuery"调用,库将自动完成后续操作。
想实现分页查询?乐观更新?条件重试?这些功能库都已内置,无需额外开发。我很少对工具感到兴奋,但这个确实是个例外。
若您因故不喜欢TanStack Query,其竞争对手SWR同样出色。两者功能全面且相似,实力相当。两者的API略有差异:TanStack Query由独立维护者管理,而SWR是Vercel的产品。因此最终选择取决于你更信任谁以及更偏好哪种API。不妨都试用一下,凭感觉做出选择。
接下来,让我们聊聊另一个热门的"远程"存储方案,它能保存与React应用相关的信息。
URL状态
网站URL中的所有变化本质上都是一种状态,你同意吗?当URL改变时,用户界面会相应调整。在现代应用中,这种响应可以是任何形式——从跳转到新页面,到根据查询参数切换当前标签页。
如今URL的基础路径部分几乎完全由外部路由器控制,且通常基于文件和文件夹结构(如Next.js所采用)。因此在状态管理语境中,我们通常不会将其纳入考量。
但查询字符串部分则截然不同。这里存储着影响特定页面细微细节的精细化信息。
考虑这个 URL:/somepath?search="test"&tab=1&sidebar=open&onboarding=step1。问号后面的所有内容都是状态参数,用于定义当前打开的标签页、侧边栏的展开状态以及引导流程所处的阶段。当 URL 发生变化时,UI 界面应实时更新。当用户在引导流程中切换步骤或点击新标签页时,URL 也应相应更新。
在早期基于 Redux 的应用中,常需大量手动实现双向同步逻辑。如今部分路由器可自动处理同步:例如 React Router 提供的 useSearchParam 钩子,其用法与状态管理完全一致:
export function SomeComponent() {
const [searchParams, setSearchParams] = useSearchParams();
// ...
}
然而其他路由器可没这么慷慨。以Next.js为例,它虽提供了读取搜索参数的便捷钩子,但若需将其与包含更新的内部状态同步,你仍需费尽周折。
若你正面临此类困境,我手头有能彻底改变你开发体验的信息。别再折腾了。手动同步本地状态与URL的过程充满痛苦和诡异的bug。改用nuqs库吧。正如TanStack Query彻底改变了远程状态管理,这个名字古怪却精妙的库同样将颠覆查询参数的管理方式。
使用该库同步引导状态与URL的效果如下:
export function MyApp() {
const [step, setStep] = useQueryState('onboarding');
return (
<>
<button onClick={() => setStep('step2')}>Next Step</button>
</>
);
}
对于选项卡,如果参数不存在,是否默认设置为1,并且使用实际数字而非字符串?好的:
export function MyApp() {
const [tab, setTab] = useQueryState('tab', parseAsInteger.withDefault(1));
return (
<>
<button onClick={() => setTab(2)}>Second tab</button>
</>
);
}
如果你曾经手动实现过上述任何功能,此刻你定会痛哭流涕。
本地状态
在后Redux时代,还有另一类状态无需依赖状态管理库——本地状态!下拉菜单是展开还是收起?工具提示是否可见?组件是否已挂载?简而言之,所有存在于组件逻辑边界内、无需与其他组件共享的信息都属于本地状态。
// the isOpen state is localized and doesn't need sharing
export function CreateIssueComponent() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>Open Create Issue Dialog</button>
{isOpen && <CreateIssueDialog onClose={() => setIsOpen(false)} />}
</>
);
}
我之所以在此提及,是因为老旧应用——尤其是过度依赖Redux的应用——往往倾向于通过自选的状态管理库来处理所有状态。但若你从零开始启动新项目,完全没有必要为这类需求引入新库。
不过当需要在不同组件间共享部分状态时,这场讨论才真正变得有趣起来。
共享状态
例如,想象一个应用程序,其左侧有一个可折叠面板,并提供几种展开/折叠该面板的方式:
- 通过点击面板右侧边缘悬浮的按钮。
- 通过用鼠标拖拽面板边缘。
- 通过点击顶部工具栏中带有对应"展开"或"折叠"图标的按钮。
- 通过键盘快捷键操作。
- 进入应用的"全屏"编辑模式(可通过点击按钮或键盘快捷键实现)。
换言之,来自应用不同模块的多个组件——它们彼此之间甚至互不相识——却都试图获取并控制侧边栏最私密的信息。这与社交媒体平台的运作逻辑如出一辙。
共享状态与属性钻取
共享此信息的一种方法是采用名为"提升状态"的技术。你需要找出所有需要该信息的组件的共同父级,将状态移至该父级,并通过 props 将其值和设置器向下传递给所有组件。结果会变得一团糟,正如你能想象的那样:
export function MyApp() {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
return (
<>
<TopBar
isSidebarOpen={isSidebarOpen}
setIsSidebarOpen={setIsSidebarOpen}
/>
<div className="content">
<Sidebar
isOpen={isSidebarOpen}
setIsOpen={setIsSidebarOpen}
/>
<div className="main">
<MainArea
isSidebarOpen={isSidebarOpen}
setIsSidebarOpen={setIsSidebarOpen}
/>
</div>
</div>
<KeyboardShortcutsController
isSidebarOpen={isSidebarOpen}
setIsSidebarOpen={setIsSidebarOpen}
/>
</>
);
}
侧边栏的状态遍布各处。由于那些除了传递信息外毫无作用的无用 props,代码变得难以阅读。重构该状态需要修改每个组件。
更糟的是,该层级中的每个组件都会在状态每次变更时重新渲染,且完全无法阻止这种情况。无论采用多少备忘机制都无济于事,因为每个组件上的每个 props 都会随状态变更而改变。这实在令人头疼。
顺带一提,这种模式被称为"属性钻取"。别误会,它确实是父子组件间共享状态的可行方案。但若需要钻取超过三层的层级结构,且在应用中出现两三次以上,那你可能就遇到麻烦了。
共享状态与Context
如果属性钻取困扰着你的应用,Context 将成为你的救星。Context 是 React 中一种特殊的数据共享机制,它允许你绕过层级结构。
其核心思想如下:当你需要将状态共享给多个独立组件时, 将该状态提取为独立组件,在该组件内部创建"Context"容器存放数据,并将其渲染在应用顶层。如此,层级结构中任何需要该状态的下层组件都能直接访问该Context。
从代码角度看,若重写前文"侧边栏"示例,实现将类似如下:创建带默认值的Context:
const SidebarContext = React.createContext({
isSidebarOpen: false,
setIsSidebarOpen: (isOpen: boolean) => {},
});
// Expose the Context via hook:
const useSidebarContext = () => React.useContext(SidebarContext);
创建持有Context的“Provider”组件:
const SidebarProvider = ({ children }) => {
const [isSidebarOpen, setIsSidebarOpen] = React.useState(false);
const value = React.useMemo(
() => ({ isSidebarOpen, setIsSidebarOpen }),
[isSidebarOpen],
);
// "value" prop is the Context value
// This is what will be available to everyone who tries to access Context
return (
<SidebarContext.Provider value={value}>
{children}
</SidebarContext.Provider>
);
};
将状态的Provider呈现出来:
// SidebarProvider wraps everything
// Everything inside will have access to the Context value
export function MyApp() {
return (
<SidebarProvider>
<TopBar />
<div className="content">
<Sidebar />
<div className="main">
<MainArea />
</div>
</div>
<KeyboardShortcutsController />
</SidebarProvider>
);
}
在需要的地方直接使用 Context 值,例如在 TopBar 组件内部某个微型按钮中,该按钮用于切换侧边栏的展开状态。
const SmallToggleSidebarButton = () => {
const { isSidebarOpen, setIsSidebarOpen } = useSidebarContext();
return (
<button
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
>
Toggle Sidebar
</button>
);
};
由于“切换”功能相当标准,我甚至可以将其实现为预定义的API,供所有使用SidebarContext的用户使用:
const SidebarProvider = ({ children }) => {
const [isSidebarOpen, setIsSidebarOpen] = React.useState(false);
const value = React.useMemo(
() => ({
isSidebarOpen,
setIsSidebarOpen,
// Introduce the "toggle" API
toggleSidebar: () => setIsSidebarOpen((prev) => !prev),
}),
[isSidebarOpen],
);
return ... // same as before
};
然后在按钮中使用它:
const SmallToggleSidebarButton = () => {
const { toggleSidebar } = useSidebarContext();
return (
<button
onClick={() => toggleSidebar()}
>
Toggle Sidebar
</button>
);
};
相较于属性钻取,这种方法的优势显著!
代码清晰可读且简洁。不再存在冗余属性。重构状态不再意味着彻底重写——只需修改实际使用该状态的组件即可。若不直接暴露状态,而是创建合理API(如toggleSidebar函数),除Provider实现本身外,其他部分可能无需重构!代码高度自包含,这永远是好事。
即便你听闻Context会损害性能,实际表现仍可能提升。因为只有使用该Context的组件会在状态变更时重新渲染。所有中间层级组件——那些移除了传递性 props 的组件——都将停止重新渲染。
但Context并非完美无缺。若问我是否建议在应用中广泛使用它,我的回答是"绝对不要!"
原因如下。
不要使用Context作为共享状态
正如你所见,我不得不引入一个"Provider"组件才能使上下文正常工作。该Provider至少应位于所有需要该上下文的组件的最近父级中。
// If someone wants access to Sidebar's data
// they need to be rendered inside SidebarProvider
export function MyApp() {
return (
<SidebarProvider>
...
</SidebarProvider>
);
}
如果我想共享其他状态,比如主题设置,自然会引入另一个Provider:
export function MyApp() {
return (
<ThemingProvider>
<SidebarProvider>
...
</SidebarProvider>
</ThemingProvider>
);
}
那么还有其他状态需要分享:
export function MyApp() {
return (
<SomethingElse>
<ThemingProvider>
<SidebarProvider>
...
</SidebarProvider>
</ThemingProvider>
</SomethingElse>
);
}
太多Provider。到了某个阶段,你可能会想把其中一些分组,但其他人无法理解分组逻辑,又在组外添加了其他Provider。部分Provider开始相互依赖。某些Provider在根层级存在时毫无意义,于是你试图将其移近终端代码——只为在下次回归重构时彻底遗忘此事,最终却因组件异常行为陷入困惑:这些依赖Context的组件竟在顶层失去了Provider。
欢迎来到"Provider地狱"。
当你需要在应用中分发一两个状态时,这类Provider就非常实用。也许你的代码中唯一可共享的内容是"主题"和"用户认证状态"。这种情况下,引入两个上下文Provider并非难事。
但状态数量稍多,系统便会迅速失控。
此时你可能会考虑将所有状态合并到单一Provider中,再通过逻辑进行分离,例如:
const SharedStateContext = React.createContext({
sidebar: {
isSidebarOpen: false,
setIsSidebarOpen: (isOpen: boolean) => {},
toggleSidebar: () => {},
},
theming: {
isDarkMode: false,
setIsDarkMode: (isDark: boolean) => {},
toggleDarkMode: () => {},
},
user: {
... // user-auth-related state
},
... // other shared states
});
const useSharedStateContext = () => React.useContext(SharedStateContext);
const SharedStateProvider = ({ children }) => {
// implement all the states here
return (
<SharedStateContext.Provider value={value}>
{children}
</SharedStateContext.Provider>
);
};
那么,整个应用程序只需一个Provider即可:
export function MyApp() {
return (
{/* One and only provider */}
<SharedStateProvider>
{/* the rest of the app */}
</SharedStateProvider>
);
}
然后,你可以在所有其他组件中通过点表示法访问所有内容:
const SmallToggleSidebarButton = () => {
// extract sidebar only
const { sidebar } = useSharedStateContext();
return (
<button
onClick={() => sidebar.toggleSidebar()}
>
Toggle Sidebar
</button>
);
};
从技术角度看,这确实可行。但从关注点分离的角度考虑是否合理尚有争议,暂且不论。更严重的问题在于性能——更准确地说,是Provider内部状态变化引发的重新渲染。
问题在于,当Context值发生变化时,所有使用该Context的组件都会重新渲染——无论它们是否实际使用了变更的状态部分。
以上例而言,即使sidebar状态未变而theming状态变更,SmallToggleSidebarButton 仍会重新渲染;反之亦然。
面对如此复杂且广泛的状态,每次交互都会导致整个应用重新渲染。小规模应用尚可接受,但若规模扩大,每次交互都可能导致侧边栏卡顿数秒——用户绝不会喜欢这种体验。
当然,切记不要过早优化,先进行性能测量。但当应用超越几个共享状态的范畴后,过度使用Provider几乎必然会带来痛苦、折磨、怪异 bug 和性能问题。当然存在缓解方案,比如将Context Providres拆解为更小的单元。但此时你本质上是在重造轮子,不如直接使用现成的状态管理库。
这终于引出了本文的核心主题——状态管理库!
共享状态和外部库
只有当你的应用程序中存在更复杂的共享状态问题,且预判上下文管理无法满足需求时,才需要考虑引入外部状态管理库。
幸运的是,由于我们已完成大量前期准备工作,因此清楚该关注哪些方面。
首先,通过选用数据管理库(如TanStack Query、SWR),我们已消除了普通应用中80%的状态管理问题。随后将URL状态迁移至nuqs,又解决了10%的难题。剩余问题仅占10%!
基于此,我对共享状态解决方案的首要要求是极简性。这最后10%的处理空间实在有限,因此我需要的是不消耗脑力的方案——配置极其简单,且不引入专属抽象术语。我希望仅凭代码直观理解其含义和行为,无需翻阅文档。
任何宣称"必须抛弃既有认知"或引入"全新概念思维"的方案,都将直接被我淘汰。这类方案或许适用于开创颠覆世界的全新编程语言,但对于普通 React 应用中少量共享状态问题而言,无疑是过度杀伤。
其次,我们始终可以通过上下文在组件间共享数据。只是上下文本身并不具备真正的可扩展性:它会引发"Provider地狱"问题,并导致不必要的、无法阻止的重新渲染。
这意味着外部库不应存在这些问题。否则有何意义?它要么只提供单一全局Provider,要么完全不提供Provider。同时应允许我们提取状态的局部片段,并确保该片段不变时组件更新不会被触发。
此外,既然我们身处React生态,该库应与React的发展方向和实现方式兼容。这意味着它需适配最新版 React,能在 SSR 和 RSC 环境中稳定运行,基于钩子机制,遵循单向数据流,并采用声明式设计。顺带一提,凡是描述中包含"信号"和"可观察对象"的方案,在此直接淘汰。反正这些东西也过不了"简洁性"测试 😅。
需要澄清的是:我并非排斥"信号"和"可观察对象"模式。只是它们需要开发者从典型的"React式"思维模式进行重大转换。更何况这些模式本身非常反直觉,学习曲线陡峭。那些在非React开发经历中未接触过这些模式的人,通常会感到非常吃力。
回到库的问题。既然它们都是开源的,我至少需要对库在未来几年内能存续抱有一定信心。这意味着该库要么足够热门,即便出现突发状况,至少还有人愿意接手维护;要么由少数维护者或具备良好开源维护声誉的公司来维护。
上述条件仅能略微提高成功概率,绝非万全之策。正因如此,我更要强调"简洁性"与"契合React理念"这两点。当库出现问题时(这种情况很可能发生),我希望能以最小的重构成本切换到新的类似解决方案。
既然标准已定,要不要给各项库打分看看结果?😅
根据《React现状》调查,最受欢迎的状态管理库是:
我对这些框架的评价如下:
简约
- 👎 Redux。绝对不行。Redux以过于复杂且冗余代码过多而臭名昭著。Redux Toolkit的诞生正是为了简化Redux,这本身就说明问题。
- 😐 Redux Toolkit。可能也不行。从当前文档看还行(很久没用了),但感觉还是要学很多概念。
- 🎉 Zustand。绝对赢家。只需创建状态并通过钩子调用,文档读两行就能成为专家。
- 👎 MobX。"信号"、"可观察对象"?直接否决。
- 👎 Jotai。引入"原子"等抽象概念?不行。
- 👎 XState。 "事件驱动"、"状态机"、"角色"、"模型"、"复杂逻辑"。绝对不简单。
一个或更少Provider
🎉 就我所见,它们要么只使用一个Provider,要么根本不使用任何Provider。
若使用状态部分未改变,则不重新渲染
- 🎉 Redux & Redux Toolkit。可通过创建选择器实现。
- 🎉 Zustand。开箱即用。
其余框架也可能以某种形式支持此功能;否则根本无人问津。但在实际选型时,尤其面对全新库时,仍需通过代码验证此项关键特性。
兼容 React 发展方向与最新特性
全面验证所有库需投入大量精力,需实际编码测试。若其他标准均达标且考虑用于下个项目,我会进行验证。
- 🤨 Redux Toolkit。需深入研究才能决定。
- 🎉 Zustand。根据使用经验(我用过很多次)支持全部功能。
- 🎉 Jotai。虽未实际使用,但作为由Zustand作者兼维护者开发的最新活跃库,极可能全面支持各项功能。
- 👎 MobX。"信号"、"可观察对象"等特性不符合"声明式"或"React范式"。不考虑。
- 👎 XState。采用"事件驱动"模式,不符合"声明式"或"React范式"。不适用。
开源项目考量
🎉 所有库均保持活跃维护,未见重大隐患。这很自然,毕竟它们都位列最受欢迎框架榜单。
那么,结果揭晓。Zustand在所有类别中胜出。这与我为任何新项目默认选择的技术栈完全吻合:TanStack Query + nuqs + Zustand。
这是否意味着Zustand是最佳选择?当然不是!😅它只是满足了我重视的标准。若采用不同标准,结果可能截然不同!例如:
若将"结构化、强意见性"列为首要标准且不介意学习曲线,那么🎉Redux Toolkit必将胜出。Zustand属于"随心所欲,无所谓"的极端,而Redux以高度结构化和强意见性著称。这种特性在大组织中尤为实用——代码编写的一致性往往凌驾于一切之上。
若我的首要需求是"高级调试体验",那么🎉 Redux Toolkit将再次胜出。其Redux DevTools插件正因如此备受推崇。
若我痴迷于"状态机"模式,且共享状态需求极为复杂(达到Figma级别的复杂度),那么🎉 XState值得深入研究。
诸如此类,你应该明白我的意思了😊。
简而言之:2025年状态管理真正的面貌
好的,以上内容的简要总结。大多数情况下,特别是当你并非在开发下一个Figma时,根本不需要所谓的"状态管理库"。将所有内容塞进Redux的时代早已过去。将状态拆解为不同关注点,你会发现针对这些关注点的解决方案远比任何"通用"状态管理库更有效。
- 远程状态。来自后端、API、数据库等数据源的处理,可交由数据获取库完成。如今TanStack Query或SWR是最主流的选择,它们能解决缓存、去重、失效处理、重试、分页、乐观更新等诸多问题,且效果远胜手动实现。
- URL查询参数状态。若路由器不支持与本地状态同步,请使用nuqs避免手动实现同步带来的巨大痛苦。
- 本地状态。实际上多数状态无需共享,这只是过去过度使用Redux的遗留问题。此类场景应采用React的
useState或useReducer。 - 共享状态。这是需要在松散关联组件间共享的数据。可采用简单的属性传递技术实现,当属性传递变得繁琐时则使用Context。仅当Context无法满足需求时,状态管理库才真正发挥价值。
这样做,你会发现大约90%的状态管理问题都会迎刃而解。剩余的状态既小巧又可预测,更容易进行逻辑推演。
而最适合它的状态管理库是...根本不存在这样的东西。请先明确对你而言重要的标准,再据此评估选项。就我而言,Zustand是我的选择,因为它极其简洁、持续维护,且与"React"的开发理念高度契合。你的选择可能截然不同,这完全没问题。