将 Redux 与 Next.js 一起使用是一种反模式吗?

Is using Redux with Next.js an anti-pattern?

我正在构建一个 Next.js 应用程序,它目前正在使用 Redux。在构建它时,我想知道是否真的有必要使用 Redux,它的使用是否实际上是一种反模式。这是我的推理:

为了在 Next.js 中正确初始化 Redux Store,您必须使用 getInitialProps 方法创建自定义 App 组件。通过这样做,您将禁用 Next.js 提供的 Automatic Static Optimization

相比之下,如果我要在客户端包含 Redux,只有在应用程序安装后,Redux 存储才会在每次服务器端导航后重置。例如,我有一个 Next.js 应用程序在客户端初始化 Redux 存储,但是当路由到 pages/projects/[id] 等动态路由时,页面是服务器端呈现的,我必须重新获取商店中的所有信息。

我的问题是:

  1. 在这种情况下,Redux 存储有什么好处?
  2. 我应该在根 App 组件中初始化商店并放弃自动静态优化吗?
  3. 在 Next.js 9.3 中使用 getStaticPropsother data fetching methods
  4. 是否有更好的方法来管理状态
  5. 我是不是漏掉了什么?

If you have a custom App with getInitialProps then the Automatic Static Optimization that Next.js provides will be disabled for all pages.

正确,如果您采用这种方法。

Is there a better way ?

是的,您可以创建一个 Redux Provider 作为包装器并包装您需要的组件,redux 上下文将自动初始化并在该组件内提供。

示例:

const IndexPage = () => {
  // Implementation
  const dispatch = useDispatch()
  // ...
  // ...
  return <Something />;
}

IndexPage.getInitialProps = ({ reduxStore }) => {
  // Implementation
  const { dispatch } = reduxStore;
  // ...
  // ...
}

export default withRedux(IndexPage)

您现在可以只对需要状态管理的页面使用 Redux,而无需禁用对整个应用程序的优化。

回答您的问题“将 Redux 与 Next.js 一起使用是一种反模式吗?

没有,但需要正确使用

有关如何完成的更多信息,请点击此处:https://github.com/vercel/next.js/tree/canary/examples/with-redux

希望对您有所帮助

我们使用 Redux 主要有两个原因。

1- 在组件之间传递数据。

如果你不使用redux,那么你需要做prop drilling。为了确定用户是否登录,我们获取数据并将其存储在 redux store 中,然后 Header 组件连接到 store 并获取身份验证信息。如果你没有使用redux,那么你需要在每个页面中获取用户,然后将其传递给Header组件。

Next.js 预渲染每个页面。这意味着 Next.js 预先为每个页面生成 HTML,而不是全部由客户端 JavaScript 完成。预渲染可以带来更好的性能和 SEO。 next-redux-wrapper 包允许您使用带有自动静态优化的 redux。如果你点击 link,有一条注释说:“Next.js 在使用 class MyApp extends App 时提供通用的 getInitialProps,它将被包装器拾取,所以你不能扩展 App因为您将选择退出自动静态优化:”。我为我的项目设置了这个包,它很容易设置。

但是使用 redux 的缺点是它没有缓存。您存储数据,然后定期重新获取数据以确保它是最新的。这是一项额外昂贵的工作。为了在 redux 中实现缓存,我们使用 reselect 库。这意味着你的项目在 redux 之上有额外的依赖,并且会让你写更多的代码。

next.js 创建了一个不错的包 swr。陈旧而重新生效。它首先 returns 来自缓存的数据(陈旧),然后发送获取请求,最后再次带来更新的数据。我选择在每个页面中使用它。

import useSWR from "swr";

export const useGetUser = () => {
     // fetcher can be any asynchronous function which returns the data. useSwr will pass "/api/v1/me" to fetcher
     const { data, error, ...rest } = useSWR("/api/v1/me", fetcher);
     // !data && !error if both true, loading:true, data=null=>!data=true, error=null => !error=true
     return { data, error, loading: !data && !error, ...rest };
   };

这里是可恢复的获取器

export const fetcher = (url: string) =>
  fetch(url).then(
    async (res: Response): Promise<any> => {
      const result = await res.json();

      if (res.status !== 200) {
        return Promise.reject(result);
      } else {
        return result;
      }
    }
  );

2- 发出 api 请求。

我为我的项目设置了 redux store,它与我设置的文本编辑器冲突。 Redux 以某种方式阻止了编辑器,我无法用我在编辑器上写的文本填充商店。所以我使用可重复使用的钩子来获取 api。乍看之下很感人,但仔细分析就会明白。

export function useApiHandler(apiCall) {
  // fetching might have one those 3 states. you get error, you fetch the data, and you start with the loading state
  const [reqState, setReqState] = useState({
    error:null,
    data:null,
    loading:true, // initially we are loading 
  });
  const handler = async (...data) => {
    setReqState({ error: null, data: null, loading: true });
    try {
      // apiCall is a separate function to fetch the data
      const res = await apiCall(...data);
      setReqState({ error: null, data: res.data, loading: false });
      alert(res.data);// just to check it 
      return res.data;
    } catch (e) {
      // short circuting in or. if first expression is true, we dont evaluate the second.
      // short circuting in and. if first expression is true, result is the second expression
      const message =
        (e.response && e.response.data) || "Ooops, something went wrong...";
      setReqState({ error: message, data: null, loading: false });
      return Promise.reject(message);
    }
  };

  return [handler, { ...reqState }];
}

一个简单的api调用函数

  const createBlog = (data) => axios.post("/api/v1/blogs", data);

然后我们就是这样使用它的:

  export const useCreateBlog = () => useApiHandler(createBlog);

设置 redux 很容易,因为人们很容易不用担心他们的应用程序的性能,他们只是设置它。在我看来,如果你有一个大型应用程序,你需要设置 redux,或者如果你熟悉 graphql,你可以使用 Apollo。这是一篇很好的文章,可以让您了解如何使用 apollo 作为状态管理。 apollo as state management。我建立了一个大型电子商务网站,并在我的新应用程序中使用了 redux,因为它相对较小,所以我不使用 next js 并使它变得更复杂。

我个人认为无论如何使用 Redux 都不是一个好主意。最好使用,例如 useContext,或者在极端需要集中存储的情况下使用 mobx。但其实有一个简单的方法可以不用getInitialProps就可以使用Redux配合SSR

这里有一点很重要 - 我给出的解决方案仅适用于你不使用服务器上每个页面的渲染 - 当在第一次渲染后遵循路线时,应用程序渲染下一页它自己的。在这个解决方案中,假设商店将在服务器端初始化一次,然后将渲染结果传输到客户端。如果你每次导航路由时都需要在服务器上呈现页面并且你需要保存商店的状态,那么也许你真的最好还是看看 next-redux-wrapper。

因此,要首先在 getServerSideProps 初始化存储,您需要按如下方式更改存储初始化文件(也许您会有其他导入):

import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly';

let storeInstance: any;
export const makeStore = (initialState: {}) => {
    storeInstance = createStore(
        Reducers,
        initialState,
        composeWithDevTools(applyMiddleware(thunkMiddleware)) // Optional, but is a handy thing
    );
    return storeInstance;
};

// initializeStore used for pages that need access to store at getServerSideProps
export const initializeStore = (preloadedState) => {
    let reInitiatedStore = storeInstance ?? makeStore(preloadedState)

    // After navigating to a page with an initial Redux state, merge that state
    // with the current state in the store, and create a new store
    if (preloadedState && storeInstance) {
        reInitiatedStore = makeStore({ ...storeInstance.getState(), ...preloadedState});
        // Reset the current store
        storeInstance = undefined;
    }

    // Keep in mind that in some cases this can cause strange
    // and difficult to track errors, so whether or not
    // to uncomment next lines depends on the architecture of your application.
    // if (typeof(window) === 'undefined') {
    //    return reInitiatedStore; // For SSG and SSR always create a new store
    // }

    // Create the store once in the client
    if (!storeInstance) {
        storeInstance = reInitiatedStore;
    }

    return reInitiatedStore;
}

之后,在getServerSideProps中需要在服务器端存储的页面中,您可以简单地使用initializeStore:

import { initializeStore } from '@Redux';

// Compnent code here...

export const getServerSideProps(context: any) {
    const reduxStore = initializeStore();
    // reduxStore = {
    // dispatch: [Function (anonymous)],
    // subscribe: [Function: subscribe],
    // getState: [Function: getState],
    // }

    // Doing something with the storage...

    const initialReduxState = storeInstance.getState(); // and get it state
    return { props: { initialReduxState, ...someProps } };
}

另外不要忘记,如果您需要访问 _app.js 中的商店,您必须将商店定义为:

const store = initializeStore(pageProps.initialReduxState);

如果您使用的是 Redux,则不需要 _app.js 上的 getInitialProps。

您可以使用 next-redux-wrapper,并用它包装 _app.js export。

存储示例,带有 next-redux-wrapper 和 thunk:

import { createStore, applyMiddleware } from 'redux';
import { createWrapper } from 'next-redux-wrapper';
import { composeWithDevTools } from 'redux-devtools-extension';

import thunkMiddleware from 'redux-thunk';
import rootReducer from './rootReducer';

const bindMiddleware = middleware => {
    return composeWithDevTools(applyMiddleware(...middleware));
};

const initStore = (initialState = {}) => {
    return createStore(rootReducer, initialState, bindMiddleware([thunkMiddleware]));
};

export const wrapper = createWrapper(initStore, { debug: true });

然后在您的 _app.js 中,您将其导出为具有

的功能组件
const App = ({ Component, pageProps }) => {
   return (
      <Component {...pageProps} />
   )
}    
export default wrapper.withRedux(App);

很有魅力。只要确保你在做水合作用 ssr -> csr.