No BS Next with Redux implementation on SSR

No BS Next with Redux implementation on SSR

The only setup you will need

Let me save you some precious time, because I've spent many hours and brain cells researching this subject.

First things firs, you will need NextJs, Redux Toolkit and Next Redux Wrapper

If you want a video, then this is for you:

npx create-next-app@latest --typescript

npm install @reduxjs/toolkit --save

npm install next-redux-wrapper react-redux --save

We'll work on a project structure like this one

ScreenHunter 177.png

In store/index.ts you will have the store configuration

import { configureStore, ThunkAction } from '@reduxjs/toolkit';
import { createWrapper } from 'next-redux-wrapper';
import { Action } from 'redux';
import profileReducer from './slices/profile';
import productReducer from './slices/product';

const makeStore = () => configureStore({
  reducer: {
    profile: profileReducer,
    product: productReducer
  },
  devTools: true
});

export type AppStore = ReturnType<typeof makeStore>;
export type AppState = ReturnType<AppStore['getState']>;
export type AppThunk<ReturnType = void> = ThunkAction<ReturnType, AppState, 
unknown, Action>;

export const wrapper = createWrapper<AppStore>(makeStore);

In store/slices/profile.ts

  • The HYDRATE action is important as it makes sure the state is passed from the server to the client side on refresh
  • All pages with getServerSideProps call the HYDRATION action on the client as well; This means that you need to make sure you don't override the client state with empty data, by checking if you have data in the action payload. More about this, here;
import { createSlice } from '@reduxjs/toolkit';
import { HYDRATE } from 'next-redux-wrapper';
import { AppState, AppThunk } from '..';

export const ProfileSlice = createSlice({
  name: 'profile',

  initialState: {
    name: null
  },

  reducers: {
    setProfileData: (state, action) => {
      state.name = action.payload;
    }
  },

  extraReducers: {
    [HYDRATE]: (state, action) => {  // IMPORTANT - for server side hydration

      if (!action.payload.profile.name) {  // IMPORTANT - for not overriding data on client side
        return state;
      }

      state.name = action.payload.profile.name;
    }
  }
});

export const { setProfileData } = ProfileSlice.actions;

export const selectProfile = (state: AppState) => state.profile;

export default ProfileSlice.reducer;

In store/slices/product.ts

import { createSlice } from '@reduxjs/toolkit';
import { HYDRATE } from 'next-redux-wrapper';
import { AppState, AppThunk } from '..';

export const ProductSlice= createSlice({
  name: 'product',

  initialState: {
    name: null
  },

  reducers: {
    setProductData: (state, action) => {
      state.name = action.payload;
    }
  },

  extraReducers: {
    [HYDRATE]: (state, action) => {
      console.log('HYDRATE', action.payload);

      if (!action.payload.product.name) {
        return state;
      }

      state.name = action.payload.product.name;
    }
  }
});

export const { setProductData } = ProductSlice.actions;

export const selectProduct = (state: AppState) => state.product;

// You can do async http calls with thunks
export const fetchProduct =
    (): AppThunk =>
      async dispatch => {
        const timeoutPromise = (timeout: number) => new Promise(resolve => 
setTimeout(resolve, timeout));

        await timeoutPromise(1000);

        dispatch(
          setProductData('BA DUM DA THUNK')
        );
      };


export default ProductSlice.reducer;

In _app.tsx

import '../styles/styles.scss';
import type { AppProps } from 'next/app';
import { wrapper } from 'app/store';

function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

export default wrapper.withRedux(MyApp);

In pages/index.tsx

import type { NextPage } from 'next'
import Head from 'next/head'
import Image from 'next/image'
import { useSelector } from 'react-redux'
import Link from 'next/link'

import { wrapper } from 'app/store'
import styles from '../styles/Home.module.css'
import { selectProfile, setProfileData } from 'app/store/slices/profile'

const Home: NextPage = (props: any) => {
  const { resolvedUrl } = props;
  const profile = useSelector(selectProfile);

  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>
          Welcome to <a href="https://nextjs.org">Next.js!</a>
          { resolvedUrl }
        </h1>
        <h2>This is the redux data: {profile.name}</h2>

        <p>
          Navigate to <Link href="/profile"><a >Profile</a></Link>
        </p>
      </main>
    </div>
  )
}

export const getServerSideProps = wrapper.getServerSideProps(store => async 
({resolvedUrl}) => {
  console.log('resolvedUrl ', resolvedUrl);

  store.dispatch(setProfileData('My Server Name'))

  return {
    props: {
      resolvedUrl
   }
  }
})

export default Home

In pages/profile.tsx

Mostly the same as in index.tsx, ecept

import { useSelector } from 'react-redux'

import { wrapper } from 'app/store'
import { selectProfile } from 'app/store/slices/profile'

const Profile: NextPage = (props: any) => {
  const profile = useSelector(selectProfile);

  return (
    <div className={styles.container}>
      <main className={styles.main}>
        <h2>This is the redux data set in index: {profile.name}</h2>
        <p>
          Navigate to <Link href="/"><a >Home</a></Link> {' | '} <Link href="/product"> <a >Product</a></Link>
        </p>
      </main>
    </div>
  )
}

export const getServerSideProps = wrapper.getServerSideProps(store => async () => {

  return {
    props: {}
  }
})

export default Profile

In pages/product.tsx

Mostly the same as in index, except you can use connect to select the data from the store; You map the state to the component props.

You also dispatch the thunks as you would dispatch any action; Just don't forget to await for the dispatch.

import { connect } from 'react-redux';

import { AppState, wrapper } from 'app/store';
import { fetchProduct } from 'app/store/slices/product';


const Product: NextPage = (props: any) => {
  const { product, profile } = props;

  return(<div>{product?.name}, {profile?.name}</div>)
}

export const getServerSideProps = wrapper.getServerSideProps(store => async () => {
  await store.dispatch(fetchProduct());

  return {
    props: {}
  };
});

const mapStateToProps = (state: AppState) => ({
  profile: state.profile,
  product: state.product
});

export default connect(mapStateToProps)(Product);

That's it! Find a project implementation on my github.