Jibin Thomas

February 2, 2020

Next.js + Redux without disabling prerendering (Updated)

Integrating redux to your server-side rendered application in nextjs while preserving prerendering

web javascript react

Next.js + Redux

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.

Nextjs + Redux build output

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.

git clone https://github.com/jibin2706/nextjs-redux.git
cd next-redux-example
npm install
npm run dev
← Back to Writing