Next.js + Redux without disabling prerendering (Updated)
Integrating redux to your server-side rendered application in nextjs while preserving prerendering
I had written a post about integrating Nextjs with Redux a few months back, but it had one downside. Static prerendering was not possible using that method even though you didn't use getInitialProps in the page components.
Here's a link to the previous post, if you are interested in having a look at it.
Static prerendering allows you to generate files in build time rather than running a server. So faster page load, since there are static files and can be cached at CDN.
The difference between the early version is that we will be using the Redux higher-order component not in the _app.js file but on the page components that require redux. For achieving this, there are few tweaks made to the redux higher-order component.
Let's follow through the instruction by setting up a new project.
npm init -y
npm install react react-dom next redux react-redux react-thunk
# Optional package
npm install redux-devtools-extension
Configuring Redux in Nextjs
This wrapper file is taken from next.js Github repository
// /lib/with-redux-store.js
import React from "react";
import { Provider } from "react-redux";
import initializeStore from "../src/store";
import App from "next/app";
export const withRedux = (PageComponent, { ssr = true } = {}) => {
const WithRedux = ({ initialReduxState, ...props }) => {
const store = getOrInitializeStore(initialReduxState);
return (
<Provider store={store}>
<PageComponent {...props} />
</Provider>
);
};
// Make sure people don't use this HOC on _app.js level
if (process.env.NODE_ENV !== "production") {
const isAppHoc =
PageComponent === App || PageComponent.prototype instanceof App;
if (isAppHoc) {
throw new Error("The withRedux HOC only works with PageComponents");
}
}
// Set the correct displayName in development
if (process.env.NODE_ENV !== "production") {
const displayName =
PageComponent.displayName || PageComponent.name || "Component";
WithRedux.displayName = `withRedux(${displayName})`;
}
if (ssr || PageComponent.getInitialProps) {
WithRedux.getInitialProps = async context => {
// Get or Create the store with `undefined` as initialState
// This allows you to set a custom default initialState
const reduxStore = getOrInitializeStore();
// Provide the store to getInitialProps of pages
context.reduxStore = reduxStore;
// Run getInitialProps from HOCed PageComponent
const pageProps =
typeof PageComponent.getInitialProps === "function"
? await PageComponent.getInitialProps(context)
: {};
// Pass props to PageComponent
return {
...pageProps,
initialReduxState: reduxStore.getState()
};
};
}
return WithRedux;
};
let reduxStore;
const getOrInitializeStore = initialState => {
// Always make a new store if server, otherwise state is shared between requests
if (typeof window === "undefined") {
return initializeStore(initialState);
}
// Create store if unavailable on the client and set it on the window object
if (!reduxStore) {
reduxStore = initializeStore(initialState);
}
return reduxStore;
};
You can use the above snippet at each page that requires a connection to the redux store. But a better way to achieve this would be to wrapping Redux HOC with a common Layout component, which will a wrapper around all the pages.
We will be wrapping the Layout component in the _app.js file, which will be used across all the pages.
// \src/components/Layout.js
import React from "react";
import { withRedux } from "../../lib/with-redux-store";
function Layout({ children }) {
return <div>{children}</div>;
}
export default withRedux(Layout);
// \pages/_app.js
import App from "next/app";
import React from "react";
import Layout from "../src/components/Layout";
class MyApp extends App {
render() {
const { Component, pageProps } = this.props;
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}
}
export default MyApp;
It allows keeping the state when navigating between pages as it wraps the Layout component, which will be available on all the pages with withReduxStore higher-order function.
Unlike in a single-page application, we will return the initialize store function instead of the store itself.
// /src/store.js
import { createStore, applyMiddleware } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
import rootReducer from "../src/reducers";
export function initializeStore(initialState = {}) {
return createStore(
rootReducer,
initialState,
composeWithDevTools(applyMiddleware(thunk))
);
Now all the components can have access to the redux store using connect function from react-redux.
From the above snippet, you can see that we were able to generate static pages of / and /about when using redux wrapper in the Layout component.
Source code
Here's the source code if you want to try it out in your machine.
cd next-redux-example
npm install
npm run dev