Next.js integrates the state manager and shared token

Time:2021-10-24

preface

Recently, the SSR framework next.js was used in the project. A series of problems such as token storage and state management will be encountered in the process. Now summarize and record them and share them with you.

Here is an article:NextJS SSR – JWT (Access/Refresh Token) Authentication with external Backend, the code corresponding to the articleCode address, written using typescript, a very good article. If you are interested, you can have a look.

Token storage

The biggest difference between SSR and spa is that SSR can distinguish between client and server, and SSR can only communicate between client and server through cookies, such as token information, which was used in spa projects in the pastlocalStorageperhapssessionStorageBut in the SSR project, the server cannot get it because it is the attribute of the browser. We can use cookies if we want both the client and the server to get it at the same time, so the token information can only be stored in cookies.

So what plug-ins do we use to set and read cookie information? There are many kinds of plug-ins, such as:

But their biggest problem is that they need to manually control reading and setting. Is there a plug-in or middleware to automatically obtain and set tokens? The answer is yes, which is what we will use nextnext-redux-cookie-wrapperPlug in. The function of this plug-in is to automatically store the data in the reducer into the cookie, and then the component will automatically get the data in the reducer from the cookie. It isnext-redux-wrapperThe plug-in is recommended, andnext-redux-wrapperThe plug-in is a plug-in for connecting store data in Redux, which will be discussed next.

Data persistence

For the SSR project, we do not recommend data persistence. In addition to the above token and user name, which need to be persistent, other data should be returned from the background interface, otherwise the purpose of using SSR (directly returning HTML with data from the server) will be lost. It is better to use Spa directly.

State management

If your project is not very large and there are not many components, you do not need to consider state management at all. You need to consider state management only when the number of components is large and the data is constantly changing.

We know that next.js is also based on react, so the react based state manager is also applicable to next.js. The more popular state management are:

here you areAn articleSpecially introduce and compare them. You can see which is more suitable for you.

Finally, we choose the lightweight version of Redux:redux-toolkit

Next we will integrateredux-toolkitPlug in and shared cookie plug-innext-redux-cookie-wrapperAnd a data communication method for connecting the next.js server and the Redux storegetServerSidePropsPlug in fornext-redux-wrapper

Integration status manager Redux and shared token information

First, we will create the next.js project. After that, we will implement the following steps to realize the integration step by step.

  1. Create the store / axios.js file
  2. Modify pages/_ App.js file
  3. Create the store / index.js file
  4. Create the store / slice / auth.js file

0. Create the store / axios.js file

The purpose of creating axios.js file is to uniformly manage Axios and facilitate the setting and acquisition of Axios in slice.

store/axios.js

import axios from 'axios';
import createAuthRefreshInterceptor from 'axios-auth-refresh';
import * as cookie from 'cookie';
import * as setCookie from 'set-cookie-parser';
// Create axios instance.
const axiosInstance = axios.create({
  baseURL: `${process.env.NEXT_PUBLIC_API_HOST}`,
  withCredentials: false,
});
export default axiosInstance;

1. Modify pages/_ App.js file

usenext-redux-wrapperThe plug-in injects the Redux store data into next.js.

pages/_app.js

import {Provider} from 'react-redux'
import {store, wrapper} from '@/store'

const MyApp = ({Component, pageProps}) => {
  return <Component {...pageProps} />
}

export default wrapper.withRedux(MyApp)

2. Create the store / index.js file

  1. use@reduxjs/toolkitIntegrate reducer and create store,
  2. usenext-redux-wrapperConnect next.js and Redux,
  3. usenext-redux-cookie-wrapperRegister slice information to share with the cookie.

store/index.js

import {configureStore, combineReducers} from '@reduxjs/toolkit';
import {createWrapper} from 'next-redux-wrapper';
import {nextReduxCookieMiddleware, wrapMakeStore} from "next-redux-cookie-wrapper";
import {authSlice} from './slices/auth';
import logger from "redux-logger";

const combinedReducers = combineReducers({
  [authSlice.name]: authSlice.reducer
});
export const store = wrapMakeStore(() => configureStore({
  reducer: combinedReducers,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().prepend(
      nextReduxCookieMiddleware({
        //Here, set the cookie data you want to share between the client and the server. I set the following three data. Just set them according to your own needs
        subtrees: ["auth.accessToken", "auth.isLogin", "auth.me"],
      })
    ).concat(logger)
}));
const makeStore = () => store;
export const wrapper = createWrapper(store, {storeKey: 'key', debug: true});

3. Create the store / slice / auth.js file

Create a slice, call the background interface through Axios, return the token and user information and save them to the reducer data, as shown in the above figurenextReduxCookieMiddlewareThe token, me and islogin information here will be automatically set and read.

store/slice/auth.js

import {createAsyncThunk, createSlice} from '@reduxjs/toolkit';
import axios from '../axios';
import qs from "qs";
import {HYDRATE} from 'next-redux-wrapper';

//Get user information
export const fetchUser = createAsyncThunk('auth/me', async (_, thunkAPI) => {
  try {
    const response = await axios.get('/account/me');
    return response.data.name;
  } catch (error) {
    return thunkAPI.rejectWithValue({errorMsg: error.message});
  }
});

//Login
export const login = createAsyncThunk('auth/login', async (credentials, thunkAPI) => {
  try {
  
    //Get token information
    const response = await axios.post('/auth/oauth/token', qs.stringify(credentials));
    const resdata = response.data;
    if (resdata.access_token) {
      //Get user information
      const refetch = await axios.get('/account/me', {
        headers: {Authorization: `Bearer ${resdata.access_token}`},
      });
      
      return {
        accessToken: resdata.access_token,
        isLogin: true,
        me: {name: refetch.data.name}
      };
    } else {
      return thunkAPI.rejectWithValue({errorMsg: response.data.message});
    }

  } catch (error) {
    return thunkAPI.rejectWithValue({errorMsg: error.message});
  }
});

//Initialization data
const internalInitialState = {
  accessToken: null,
  me: null,
  errorMsg: null,
  isLogin: false
};

// reducer
export const authSlice = createSlice({
  name: 'auth',
  initialState: internalInitialState,
  reducers: {
    updateAuth(state, action) {
      state.accessToken = action.payload.accessToken;
      state.me = action.payload.me;
    },
    reset: () => internalInitialState,
  },
  extraReducers: {
    //Then, get the server-side reducer and inject it into the client-side reducer to achieve the purpose of data unification
    [HYDRATE]: (state, action) => {
      console.log('HYDRATE', state, action.payload);
      return Object.assign({}, state, {...action.payload.auth});
    },
    [login.fulfilled]: (state, action) => {
      state.accessToken = action.payload.accessToken;
      state.isLogin = action.payload.isLogin;
      state.me = action.payload.me;
    },
    [login.rejected]: (state, action) => {
      console.log('action=>', action)
      state = Object.assign(Object.assign({}, internalInitialState), {errorMsg: action.payload.errorMsg});
      console.log('state=>', state)
      // throw new Error(action.error.message);
    },
    [fetchUser.rejected]: (state, action) => {
      state = Object.assign(Object.assign({}, internalInitialState), {errorMsg: action.errorMsg});
    },
    [fetchUser.fulfilled]: (state, action) => {
      state.me = action.payload;
    }
  }
});

export const {updateAuth, reset} = authSlice.actions;

This completes the integration of all plug-ins. Then we run the web page, log in and enter the user name and password. You will find that the above data is saved in the cookie in the form of password.

Next.js integrates the state manager and shared token

Login page code:

pages/login.js

import React, {useState, useEffect} from "react";
import {Form, Input, Button, Checkbox, message, Alert, Typography} from "antd";
import Record from "../../components/layout/record";
import styles from "./index.module.scss";
import {useRouter} from "next/router";
import {useSelector, useDispatch} from 'react-redux'
import {login} from '@/store/slices/auth';
import {wrapper} from '@/store'


const {Text, Link} = Typography;
const layout = {
  labelCol: {span: 24},
  wrapperCol: {span: 24}
};
const Login = props => {
  const dispatch = useDispatch();
  const router = useRouter();
  const [isLoding, setIsLoading] = useState(false);
  const [error, setError] = useState({
    show: false,
    content: ""
  });

  function closeError() {
    setError({
      show: false,
      content: ""
    });
  }

  const onFinish = async ({username, password}) => {
    if (!username) {
      setError({
        show: true,
        Content: "please enter user name"
      });
      return;
    }
    if (!password) {
      setError({
        show: true,
        Content: "please enter the password"
      });
      return;
    }
    setIsLoading(true);
    let res = await dispatch(login({
      grant_type: "password",
      username,
      password
    }));
    if (res.payload.errorMsg) {
      message.warning(res.payload.errorMsg);
    } else {
      router.push("/");
    }
    setIsLoading(false);
  };

  function render() {
    return props.isLogin ? (
      <></>
    ) : (
      <div className={styles.container}>
        <div className={styles.content}>
          <div className={styles.card}>
            <div className={styles.cardBody}>
              <div className={styles.error}>{error.show ?
                <Alert message={error.content} type="error" closable afterClose={closeError}/> : null}</div>
              <div className={styles.cardContent}>
                <Form
                  {...layout}
                  name="basic"
                  initialValues={{remember: true}}
                  layout="vertical"
                  onFinish={onFinish}
                  // onFinishFailed={onFinishFailed}
                >
                  <div className={styles.formlabel}>
                    <b>User name or mailbox</b>
                  </div>
                  <Form.Item name="username">
                    <Input size="large"/>
                  </Form.Item>
                  <div className={styles.formlabel}>
                    <b>Code</b>
                    <Link href="/account/password_reset" target="_blank">
                      Forget password
                    </Link>
                  </div>
                  <Form.Item name="password">
                    <Input.Password size="large"/>
                  </Form.Item>

                  <Form.Item>
                    <Button type="primary" htmlType="submit" block size="large" className="submit" loading={isLoding}>
                      {isloding? "Logging in...": "logging in"}
                    </Button>
                  </Form.Item>
                </Form>
                <div className={styles.newaccount}>
                  First use seaurl? {" "}
                  <Link href="/join?ref=register" target="_blank">
                    Create an account
                  </Link>
                  {/* <a className="login-form-forgot" href="" >
                                    Create an account</a> */}
                </div>
              </div>
            </div>

            <div className={styles.recordWrapper}>
              <Record/>
            </div>
          </div>
        </div>
      </div>
    );
  }

  return render();
};

export const getServerSideProps = wrapper.getServerSideProps(store => ({ctx}) => {
  const {isLogin, me} = store.getState().auth;
  if(isLogin){
    return {
      redirect: {
        destination: '/',
        permanent: false,
      },
    }
  }
  return {
    props: {}
  };
});

export default Login;

be careful

1. Usednext-redux-wrapperHydrate must be added to synchronize the server and client reducer data, otherwise the data of the two ends are inconsistent, resulting in conflict

[HYDRATE]: (state, action) => {
      console.log('HYDRATE', state, action.payload);
      return Object.assign({}, state, {...action.payload.auth});
    },

2. Attentionnext-redux-wrapperandnext-redux-cookie-wrapperedition

"next-redux-cookie-wrapper": "^2.0.1",
"next-redux-wrapper": "^7.0.2",

summary

1. Instead of using persistence, SSR projects directly request data from the server side interface for direct rendering, otherwise the significance of using SSR will be lost,
2. Next.js is divided into static rendering and server-side rendering. In fact, if your SSR project is small or static data, you can consider using the client-side static method directlygetStaticPropsTo render.

quote

redux-toolkit

next-redux-cookie-wrapper

next-redux-wrapper

nextjs-auth

Next.js DEMO next-with-redux-toolkit

Recommended Today

SQL exercise 20 – Modeling & Reporting

This blog is used to review and sort out the common topic modeling architecture, analysis oriented architecture and integration topic reports in data warehouse. I have uploaded these reports to GitHub. If you are interested, you can have a lookAddress:https://github.com/nino-laiqiu/TiTanI recorded a relatively complete development process in my hexo blog deployed on GitHub. You can […]