image.png

为什么需要重新考虑 Zustand 与 Next.js 的结合?

在现代 Web 开发中,状态管理是一个不可或缺的环节。Zustand 作为一款轻量、简洁的 React 状态管理库,因其不依赖 Context Provider 而备受开发者青睐,常被认为是 Redux 的高效替代品。

但在与 Next.js 集成时,尤其是在服务器端渲染(SSR)和客户端状态水合(Hydration)场景中,Zustand 的设计理念与 Next.js 的运行机制存在一定冲突。本文将深入探讨这一问题,并提供相应的解决思路。

Zustand 的优势与设计理念

"Zustand" 在德语中意为“状态”,这款库的核心目标是为 React 提供一个极简的状态管理方案。与传统的 Redux 不同,Zustand 避免了繁琐的 Context Provider 配置,仅需几行代码即可实现状态的定义与使用。这种极简的设计让它在开发者中迅速流行,尤其适用于小型到中型的项目。

Zustand 与 Next.js 的冲突点

Zustand 在默认设计中并不依赖 Provider,这在纯客户端环境中表现出色。但在使用 Next.js 进行服务器端渲染 (SSR) 时,状态的同步问题变得复杂。问题的根源在于,服务器生成的初始状态需要在客户端重新“水合”回 Zustand,而这种同步机制需要显式的 Provider 来协助完成。

根据 Zustand 官方的推荐方案:

使用 Zustand 进行 SSR 时,需要通过自定义 Provider 确保服务器端的状态能够正确传递到客户端。

这就导致了一个矛盾——Zustand 本应避免使用 Provider,但在 Next.js 中却需要它。这种转变不仅增加了开发的复杂度,还可能让习惯了“无 Provider”哲学的开发者感到困惑。

社区的反馈和建议

在 GitHub 讨论区和 Zustand 的社区中,关于这一问题的讨论十分热烈。许多开发者提出了以下的担忧:

  • 额外的开发成本:为了解决 SSR 问题,需要手动实现 Provider 和状态的水合逻辑,显著增加了复杂度。
  • 哲学冲突:开发者使用 Zustand 的一个重要原因是其“无 Provider”理念,而 SSR 的实现却与这一理念背道而驰。

如何应对这种冲突?

针对上述问题,以下是几种可行的应对策略:

1. 自定义 Provider 方案

接受在 Next.js 中使用 Provider 的必要性。虽然这与 Zustand 的初衷不符,但通过封装自定义 Provider,可以减少开发者的心智负担。关键在于,将状态的初始化和水合逻辑清晰地封装成一个独立的模块,确保可复用性。

// stores/StoreProvider.js
import { createContext, useContext } from 'react';
import useStore from './useStore';

const StoreContext = createContext();

export function StoreProvider({ children, initialZustandState }) {
  const store = useStore(initialZustandState);
  return (
    <StoreContext.Provider value={store}>
      {children}
    </StoreContext.Provider>
  );
}

export function useStoreContext() {
  return useContext(StoreContext);
}

2. 中间件支持

使用中间件来帮助管理 Zustand 在 SSR 和水合过程中的状态。中间件可以在 Next.js 的服务端钩子中捕获 Zustand 的状态,并将其注入到页面的 props 中。这样可以减少客户端的水合逻辑。

3. 考虑替代的状态管理方案

如果项目的 SSR 需求较为复杂,考虑使用更成熟的状态管理方案,如 Redux 或 Recoil。这些库在 SSR 场景中的文档和示例更为完善,开发者的学习成本相对较低。

实战示例:如何在 Next.js 中使用 Zustand

以下是一个完整的示例,展示了如何在 Next.js 中配置 Zustand 并实现状态的 SSR 和水合。

步骤 1: 安装依赖

npm install zustand

步骤 2: 创建一个自定义的 Provider

// stores/StoreProvider.js
import { createContext, useContext } from 'react';
import useStore from './useStore';

const StoreContext = createContext();

export function StoreProvider({ children, initialZustandState }) {
  const store = useStore(initialZustandState);
  return (
    <StoreContext.Provider value={store}>
      {children}
    </StoreContext.Provider>
  );
}

export function useStoreContext() {
  return useContext(StoreContext);
}

步骤 3: 在 _app.js 中初始化状态

// pages/_app.js
import App from 'next/app';
import { StoreProvider } from '../stores/StoreProvider';
import useStore from '../stores/useStore';

function MyApp({ Component, pageProps, initialZustandState }) {
  return (
    <StoreProvider initialZustandState={initialZustandState}>
      <Component {...pageProps} />
    </StoreProvider>
  );
}

MyApp.getInitialProps = async (appContext) => {
  const appProps = await App.getInitialProps(appContext);
  const store = useStore.getState();
  return { ...appProps, initialZustandState: store };
};

export default MyApp;

步骤 4: 在客户端完成状态水合

// pages/_app.js
import { useEffect } from 'react';
import useStore from '../stores/useStore';

function MyApp({ initialZustandState }) {
  useEffect(() => {
    useStore.setState(initialZustandState);
  }, [initialZustandState]);
}

步骤 5: 在组件中使用 Zustand 的状态

// components/Counter.js
import { useStoreContext } from '../stores/StoreProvider';

function Counter() {
  const { count, increment, decrement } = useStoreContext();

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

export default Counter;

总结

Zustand 的“无 Provider”设计虽然简单高效,但在与 Next.js 集成时,开发者需要额外考虑 SSR 和水合的问题。通过引入自定义的 Provider、中间件支持或切换到更成熟的状态管理工具,开发者可以更轻松地解决这些问题。未来,社区可能会提供更标准化的解决方案,但在此之前,理解这些细节对于构建高性能的 Next.js 应用至关重要。

首发于公众号 大迁世界,欢迎关注。📝 每周一篇实用的前端文章 🛠️ 分享值得关注的开发工具 ❓ 有疑问?我来回答

本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试完整考点、资料以及我的系列文章。


王大冶
68.1k 声望105k 粉丝