66
votes

I'm building a Next.js app and it currently is using Redux. As I am building it I am wondering if the use of Redux is really necessary, and if its use is actually an anti-pattern. Here is my reasoning:

In order to properly initialize the Redux Store in Next.js, you must create a custom App component with a getInitialProps method. By doing this you are disabling the Automatic Static Optimization that Next.js provides.

By contrast, if I were to include Redux on the client side, only after the App has mounted, then the Redux store will reset after every server side navigation. For instance, I have a Next.js app that initializes the Redux store on the client side, but when routing to a dynamic route such as pages/projects/[id], the page is server side rendered, and I have to re-fetch any information that was in the store.

My questions are:

  1. What are the benefits of a Redux store in this circumstance?
  2. Should I initialize the store in the root App component and forego the Automatic Static Optimization?
  3. Is there a better way to do to manage state in Next.js 9.3 with getStaticProps and the other data fetching methods
  4. Am I missing something?
3

3 Answers

47
votes

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

True, if you follow this approach.

Is there a better way ?

Yes, you can create a Redux Provider as a wrapper and wrap the component you need, the redux context will be automatically initialized and provided within that component.

Example:

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

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

export default withRedux(IndexPage)

You have now the possibility to use Redux only for the pages which need state management without disabling the optimization for the entire App.

Answering you question "Is using Redux with Next.js an anti-pattern?"

No, but it needs to be used properly.

More info on how is done here: https://github.com/vercel/next.js/tree/canary/examples/with-redux

I hope this helps

13
votes

we use Redux mainly for 2 reasons.

1- pass data between components.

if you do not use redux, then you need to do prop drilling. To decide if user logged in or not, we fetch the data and then store it in redux store and then Header components connects to the store and gets the authentication info. If you are not using redux, then you need to fetch the user in each page and then pass it to the Header component.

Next.js pre-renders every page. This means that Next.js generates HTML for each page in advance, instead of having it all done by client-side JavaScript. Pre-rendering can result in better performance and SEO. next-redux-wrapper package allows you to use the redux with automatic-static-optimization. If you click on the link, there is a note saying: "Next.js provides generic getInitialProps when using class MyApp extends App which will be picked up by wrapper, so you must not extend App as you'll be opted out of Automatic Static Optimization:". I set up this package for my project and it is easy to setup.

But downside of using redux, it is not caching. You store the data and then you refetch it periodically to make sure it is up to date. and this is an extra expensive work. To achieve caching in redux, we use reselect library. This means extra dependency for your project on top of redux and will make you write more code.

There is a nice package swr which is created by next.js. Stale-While-Revalidate. it first returns the data from cache(stale), then sends the fetch request, and finally comes with the updated data again. I choose the use this in each page.

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 };
   };

here is resuable fetcher

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- Making api requests.

I set up redux store for my project and it was conflicting with the text-editor that I set up. Redux was somehow blocking the editor and i could not populate the store with the text that i wrote on the editor. So I used reusable hooks for fetching api. it looks intimating in the beginning but if you analyze it, it will make sense.

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 }];
}

A simple apiCall function

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

and then this is how we use it :

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

Setting redux is easy since it is easy people are not worried about the performance of their app, they just set it up. In my opinion, if you have a large app you need to set up redux or if you are familiar with graphql you can use Apollo. Here is a good article to get an idea about using apollo as state management. apollo as state management. I built a large ecommerce website and I used redux, my in my new app, since it is relatively small I do not use next js and make it more complicated.

1
votes

Personally I think using the Redux is not a good idea at any case. It would be better to use, for example, useContext, or in case of extreme need for centralized storage look towards mobx. But in fact, there is a simple way to use Redux with SSR without using getInitialProps.

There is an important point here - the solution I gave is applicable only if you DO NOT use the rendering of literally every page on the server - when following the route after the first render, the application renders the next page on its own. In this solution it is assumed that the store will be initialized on the server side once and then the rendering result will be transferred to the client. If you need to render the page on the server absolutely every time you navigate the route and you need to save the state of store, then perhaps you really better still look towards the next-redux-wrapper.

So to initialize store at getServerSideProps first you will need to change your storage initialization file as follows (perhaps you will have other imports):

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;
}

After that, in the page, where you need store on server side in the getServerSideProps, you can simple use 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 } };
}

Also don't forget that if you need to access the store in your _app.js, you must define store as:

const store = initializeStore(pageProps.initialReduxState);