preface
SSR framework used in recent projects next JS, you will encounter a series of problems such as token storage and state management. Now summarize and record them and share them with you.
Token storage
The biggest difference between SSR and SPA is that SSR distinguishes between Client and Server, and SSR can only communicate between Client and Server through cookies, such as token information. In the past, we used to use localStorage or sessionStorage in SPA projects, but in SSR projects, the Server can't get it because it is the attribute of the browser, We can use cookies if both the Client and the Server can get them at the same time, so the token information can only be stored in cookies.
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 token s? The answer is yes. Next we will use the next Redux cookie wrapper plug-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 is recommended by the next Redux wrapper plug-in, which is a plug-in connecting the store data in redux, I'll talk about it 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 next JS is also based on React, so the React based state manager is also applicable to next js
Integration status manager Redux and shared Token information
First, we create next After the JS project is created, we implement the following steps to realize the integration step by step.
- Create store / Axios JS file
- Modify pages/_app.js file
- Create store / index JS file
- Create store / slice / auth JS file
0. Create store / Axios JS file
Create axios The purpose of 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
Use the next Redux wrapper plug-in to inject 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 store / index JS file
- Use @ reduxjs/toolkit to integrate reducer and create a store,
- Use next redux wrapper to connect to next JS and redux,
- Use the next Redux cookie wrapper to register the slice information to be shared to 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});
If you want to develop applets or learn more about applets, you can help you realize your development needs through a third-party professional development platform: Xiamen cares about technology -Focus Xiamen applet customization development , app development, website development, H5 game development
3. Create store / slice / auth JS file
Create a slice, call the background interface through axios, return the token and user information and save them in the reducer data. The above nextreuxcookiemiddleware will automatically set and read the token, me and isLogin information here.
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}); } }); // Sign in 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.
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: "enter one user name" }); return; } if (!password) { setError({ show: true, content: "Please input a 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>password</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..." : "Sign 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. HYDRATE must be added when next Redux wrapper is used. The purpose is to synchronize the data of server and client reducer. 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. Note the next Redux wrapper and next Redux cookie wrapper versions
"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 all static data, you can consider directly using the client-side static method getStaticProps to render.