All Articles

Setting up Next.js with Redux

Published by Tony Vu on Jan 18, 2021

In a traditional client side rendered React app, you only need to account for the state on the client slide.

By comparison, in Next.js, server rendered pages and components that depend on state will need to pull from the Redux store. This is because server rendered pages generate HTML in advance instead of being sent the client. To handle this, you need to create a Redux store on the server side. Once the store is created, it utilizes the state from the store to preload any data. The state is then sent to the client along with the pre-rendered page. The client receives the pre-rendered page and initialize its own Redux store with the state from the server.

This article shows you how to do all of this by creating a job posting form with a live preview alongside of it. You should take a look at my other article on styling a simple form using Tailwind CSS if you’d like to learn how I styled the form in this article.

To do this, you will do the following

  1. Install Redux dependencies and next-redux-wrapper
  2. Create actions
  3. Create reducers
  4. Create the Redux store
  5. Provide the store to all pages

Before getting started, scaffold a Next.js app by running create-next-app. This article won’t go over how to use Next.js. You are encouraged to take a look at the Next.js docs if you’re not familiar with how Next.js works.

npx create-next-app job-form

Install Redux dependencies and next-redux-wrapper

As usual, you will need to install the Redux libraries needed to wire up the store with React. The next-redux-wrapper is a higher order component that exports a wrapper. This wrapper will be used in the App component to automatically create an instance of the store and make it accessible in all Next.js pages.

npm install next-redux-wrapper react-redux redux

Create actions

Let’s create our actions next. This is no different than how it would be done in a client side React app.

First, create an actions folder in the root of your project directory. Within that folder, create types.js and index.js

types.js

export const CHANGE_JOB_TITLE = "CHANGE_JOB_TITLE";
export const CHANGE_LOCATION = "CHANGE_LOCATION";
export const CHANGE_DESCRIPTION = "CHANGE_DESCRIPTION";
export const CHANGE_JOB_TYPE = "CHANGE_JOB_TYPE";

Next, create the index.js file

index.js

import {
  CHANGE_JOB_TITLE,
  CHANGE_JOB_TYPE,
  CHANGE_LOCATION,
  CHANGE_DESCRIPTION,
} from "./types";

export const changeJobTitle = (title) => {
  return {
    type: CHANGE_JOB_TITLE,
    payload: title,
  };
};

export const changeLocation = (location) => {
  return {
    type: CHANGE_LOCATION,
    payload: location,
  };
};

export const changeDescription = (description) => {
  return {
    type: CHANGE_DESCRIPTION,
    payload: description,
  };
};

export const changeJobType = (jobType) => () => {
  return {
    type: CHANGE_JOB_TYPE,
    payload: jobType,
  };
};

Create reducers

Let’s create our reducers. Similarly to our actions, this is no different than the boilerplate code on a client side React app.

First, create a reducers folder in the root of your project directory. Within that folder, create formReducer.js and index.js

formReducer.js

import {
  CHANGE_JOB_TITLE,
  CHANGE_LOCATION,
  CHANGE_DESCRIPTION,
  CHANGE_JOB_TYPE,
} from../actions/types”;

const initialState = {
  job_title: “”,
  location: “”,
  description: “”,
  job_type: null,
};

const formReducer = (state = initialState, action) => {
  switch (action.type) {
    case CHANGE_JOB_TITLE:
      return {
        …state,
        job_title: action.payload,
      };
    case CHANGE_LOCATION:
      return {
        …state,
        location: action.payload,
      };
    case CHANGE_DESCRIPTION:
      return {
        …state,
        description: action.payload,
      };
    case CHANGE_JOB_TYPE:
      return {
        …state,
        job_type: action.payload,
      };
    default:
      return state;
  }
};

export default formReducer;

index.js

import { combineReducers } from “redux”;
import formReducer from./formReducer”;

export default combineReducers({
  form: formReducer,
});

Create the Redux store

Create a store folder from the root of your project directory. Add the store.js where your store will be created

store.js

import { createStore } from "redux";
import { createWrapper } from "next-redux-wrapper";

import rootReducer from "../reducers";
const initialState = {};

const makeStore = (context) => createStore(rootReducer, initialState);

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

The key difference in setting up Redux between a Next.js app and a traditional React app is in the way the store is provided to your app. In Next.js, you will be using next-redux-wrapper to initialize the store for you and make it available on every client request. Using a higher order component, next-redux-wrapper, handles creating a new store each time a client makes a request. The client then receives the store and uses it to initialize its own Redux store.

Provide the store to all pages

After creating the Redux boilerplate and creating the store, you will now need to use createWrapper from next-redux-wrapper to provide the store to all pages. Create pages/_app.js, which is your custom App component, to initialize all pages with the store as follows. For reference you can read more about the customer App component in the Next.js docs.

pages/_app.js

import { createStore } from "redux";
import { createWrapper } from "next-redux-wrapper";

import rootReducer from "../reducers";
const initialState = {};

const makeStore = (context) => createStore(rootReducer, initialState);

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

Finally, let’s create the form. Create the folder pages/jobs . Within the folder, create new.js. This will contain the form that will utilize our actions. As you can see, you will be using connect to wire up this form component to the Redux store.

pages/new.js

import React from "react";
import { useForm } from "react-hook-form";
import { connect } from "react-redux";
import JobPreview from "../../components/JobPreview";
import {
  changeJobTitle,
  changeLocation,
  changeDescription,
} from "../../actions";

const JobForm = ({
  changeJobTitle,
  changeLocation,
  changeDescription,
  changeJobType,
}) => {
  const { register, errors, handleSubmit } = useForm();

  const onSubmit = async (data) => {
    const fields = { fields: data };
  };

  return (
    <React.Fragment>
      <div className="grid grid-cols-2">
        <div>
          <h1 className="mt-5 text-center text-3xl font-semibold">
            Post a new job
          </h1>
          <form
            className="max-w-2xl m-auto py-10 mt-10 px-12 border"
            onSubmit={handleSubmit(onSubmit)}
          >
            <label className="text-gray-600 font-medium">Job Title</label>
            <input
              className="border-gray-300 border py-1 px-2  w-full rounded text-gray-700"
              name="title"
              autoFocus
              onChange={(e) => changeJobTitle(e.target.value)}
              ref={register({
                required: "Please enter a job title",
              })}
            />
            {errors.title && (
              <div className="mb-3 text-normal text-red-500 ">
                {errors.title.message}
              </div>
            )}

            <label className="text-gray-600 font-medium block mt-4">
              Location
            </label>
            <input
              className="border-solid border-gray-300 border py-1 px-2 w-full rounded text-gray-700"
              name="location"
              type="text"
              placeholder="Scranton, PA"
              onChange={(e) => changeLocation(e.target.value)}
              ref={register({
                required: "Please enter a location",
              })}
            />
            {errors.location && (
              <div className="mb-3 text-normal text-red-500 ">
                {errors.location.message}
              </div>
            )}

            <label className="text-gray-600 font-medium block mt-4">
              Description
            </label>

            <input
              className="border-solid border-gray-300 border py-1 px-2 w-full rounded text-gray-700"
              name="link"
              type="text"
              onChange={(e) => changeDescription(e.target.value)}
              ref={register({
                required: "Please enter a description",
              })}
            />
            {errors.description && (
              <div className="mb-3 text-normal text-red-500 ">
                {errors.description.message}
              </div>
            )}

            {errors.jobtype && (
              <div className="mb-3 text-normal text-red-500 ">
                {errors.jobtype.message}
              </div>
            )}

            <button
              className="mt-4 w-full bg-green-400 hover:bg-green-600 text-green-100 border py-3 px-6 font-semibold text-md rounded"
              type="submit"
            >
              Submit
            </button>
          </form>
        </div>
        <div>
          <JobPreview />
        </div>
      </div>
    </React.Fragment>
  );
};

export default connect((state) => state, {
  changeJobTitle,
  changeLocation,
  changeDescription,
})(JobForm);

Did this post help you or do you have any comments or questions? Email me at hey@tonyvu.co or Tweet me at @tonyv00