The way of real server rendering 04 - redux-01

All the source code, documents and pictures are in the warehouse of github, Click to enter the warehouse

Related reading

1. redux

  • After routing, we need to consider the data. We use the most widely used redux to manage the data state of react
  • There are three ways to update a store

    • Synchronization, which includes the unified update of the client and the server
    • The client is asynchronous. This is the client that we usually use to send requests, get data asynchronously, and then modify the store value
    • The server is asynchronous, which is more complex. Please refer to the next section
  • So this section mainly introduces synchronous update store and asynchronous update store of client

1.1 INTRODUCTION

  • There are two kinds of store creation: one is the client, the other is the server, and the store of each end should be separated. As a method call, if the purpose of this is the client, each user has a client, which uses the data in their own store, but the server is different. No matter how many clients there are, there is only one server , so, in order to avoid the confusion of each user's store data, we use the server store as a method call, so that when each user calls the server store, they have their own method, which calls their own data, so that the data will not be confused
  • The method of using store by the client is the same as that of normal client rendering, there is no difference
  • The server uses store only by wrapping a layer of Provider outside the StaticRouter and passing it into the server store

1.2 required libraries for Redux

  • npm i redux react-redux redux-thunk redux-logger -S
  • npm i redux-devtools-extension -D
  • redux, this is the core library of redux
  • React redux. Because react and Redux are completely unrelated, they can be used independently of each other. Redux can be directly introduced into react, but it's more cumbersome and inconvenient to use. So we use react Redux for the convenience of establishing a connection between react and redux
  • redux thunk, let redux use a method when dispatch ing. Here we use the method mainly for asynchronous data acquisition
  • Redux logger, which displays the record of state changes on the console
  • Redux devtools extension, this is a Google browser redux This plug-in needs to be enabled by middleware to view the state change status

2. Use the simplest redux

  • Because the code is relatively simple, there is little difference between our clients' use of redux, so you can see the code directly
  • There are three functions here

    • The first is to directly obtain the data in redux and the value of username under user
    • The second is to click the button to modify the value of age under user after obtaining the data
    • The third is that the client calls the third-party interface to obtain data and modify the value of the schoolList under the user

2.1 create store

  • store/index.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import { composeWithDevTools } from 'redux-devtools-extension';

import reducers from './reducers';

export const getServerStore = () => createStore(
  reducers,
  composeWithDevTools(applyMiddleware(thunk, logger))
);

export const getClientStore = () => createStore(
  reducers,
  composeWithDevTools(applyMiddleware(thunk, logger))
);
  • store/reducers.js
import { combineReducers } from 'redux';

import userReducer from './user/reducer';

export default combineReducers({
  user: userReducer
});
  • store/user/actionTypes.js
export const SET_INCREMENT_AGE = 'SET_INCREMENT_AGE';

export const GET_SCHOOL_LIST = 'GET_SCHOOL_LIST';
  • store/user/createActions.js

    • As for the value of action, some people like to use payload, and some people like to use the variable name of the required value directly. This can be used in any way, as long as it is consistent, there is no mandatory specification.
    • Since payloads are used in Redux logger, it is recommended to use payloads
    • This is actually an attribute value, as long as the defined value in actions is the same as that obtained in reducer
    • Note: the interface here is a simulated interface, which is the interface service in Section 2.4 below. You can simply define an interface to make ajax request response data
import * as Types from './actionTypes';
import axios from 'axios';

export const incrementAge = () => {
  return {
    type: Types.SET_INCREMENT_AGE
  }
};

export const getSchoolList = () => {
  return (dispatch) => {
    return axios.get('http://localhost:8758/api/getSchoolList').then(res => {
      if (res.status === 200) {
        let schoolList = res.data.schoolList;
        console.log(res.data);
        dispatch({
          type: Types.GET_SCHOOL_LIST,
          payload: schoolList
        });
      }
    });
  }
}
  • store/user/reducer.js
import * as Types from './actionTypes';

const initState = {
  name: 'mark',
  age: 18,
  schoolList: []
};

export default (state = initState, action) => {
  switch (action.type) {
    case Types.SET_INCREMENT_AGE:
      return { ...state, age: state.age + 1 };
    case Types.GET_SCHOOL_LIST:
      console.log(action);
      return { ...state, schoolList: action.payload };
    default:
      return { ...state };
  }
}

2.2 modification of routing file

  • The reason why the routing file needs to be modified is that it doesn't matter if it is modified here, but it also needs to be modified in the next section, which is also relatively simple, so it should be directly modified here to avoid confusion with the next section
  • That's how our route was written before
export default (
  <>
    <Route path='/' exact component={Home}/>
    <Route path='/news' component={News}/>
  </>
);
  • Now we change to the form of array object, because it is convenient for us to load asynchronous data on components
export default [
  {
    path: '/',
    component: Home,
    exact: true,
    key: '/'
  },
  {
    path: '/news',
    component: News,
    exact: true,
    key: '/news'
  }
];
  • Then we iterate between the client and the server, and then change the assembly to the form of Route. After a careful look at the two writing methods, there's no difference. It's just a change of form. For the convenience of later use
{
  routes.map(route => <Route {...route} />)
}

2.2 redux under the client

  • client/index.js
import { Provider } from 'react-redux';
import { Route } from 'react-router-dom';
import { getClientStore } from "../store";

hydrate(
  <Provider store={getClientStore()}>
    <BrowserRouter>
      <>
        <Header/>
        <div className="container" style={{ marginTop: 70 }}>
            {
              routes.map(route => <Route {...route} />)
            }
          </div>
      </>
    </BrowserRouter>
  </Provider>, window.root);
  • containers/Home/index.js

    • As for the use of connect in react Redux, you can use connect as a decorator of a component or as a function call directly. Because the decorator is actually the syntactic sugar of multiple function calls, I write connect as a function call
    • For the parameter of connect, you can write the method directly in the parameter, or you can define mapStateToProps and mapDispatchToProps as the method first, and then take the method as the parameter directly
    • As for the method call in actions, the method I use here is actually somewhat complicated. The simplest is to call the method in actions directly inside the component. Here I define A method A in the component and A method B in the props of the component, if the method in actions is C. Then the simplest method is to call this.props.C() directly, but my order here is as follows: call A() first, then A() call B(), and finally call c() in B(). Specific how to call it, according to personal preferences.
    • Here, it's not my chicken thief. It's OK to use it anyway. Actually, there's no standard way to write it. I still like the current writing method. It's clear and clear. It's very convenient to pass parameters and call anything. The disadvantage is that there's a lot of code and there are many changes when modifying
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as UserActions from '../../store/user/createActions';

class Home extends Component {

  state = {
    number: 0
  };

  handleClick = () => {
    this.setState({
      number: this.state.number + 1
    });
  };

  incrementAge = () => {
    this.props.propIncrementAge();
  };

  getSchoolList = () => {
    this.props.propGetSchoolList();
  }

  render() {
    return (
      <div>
        <h2>HELLO, HOME PAGE</h2>
        <h2>
          <button className="btn btn-primary" onClick={this.handleClick}>click</button>
          &nbsp;&nbsp;&nbsp;&nbsp;
          <span>{this.state.number}</span>
        </h2>
        <ul className="list-group">
          <li className="list-group-item">name: {this.props.user.name}</li>
          <li className="list-group-item">
            <button className="btn btn-primary" onClick={this.incrementAge}>increment age</button> &nbsp;&nbsp;&nbsp;&nbsp;
            <span>{this.props.user.age}</span></li>
        </ul>
        <h2>
          <button className="btn btn-primary" onClick={this.getSchoolList}>schoolList</button>
        </h2>
        <ul className="list-group">
          {
            this.props.user.schoolList.map(school => (
              <li key={school.id} className="list-group-item">
                {school.id}. {school.name}
              </li>
            ))
          }
        </ul>
      </div>
    );
  }
}

const mapStateToProps = state => ({
  user: state.user
});

const mapDispatchToProps = dispatch => ({
  propIncrementAge() {
    dispatch(UserActions.incrementAge());
  },
  propGetSchoolList() {
    dispatch(UserActions.getSchoolList());
  }
})

export default connect(mapStateToProps, mapDispatchToProps)(Home);

2.3 redux on the server

  • In the synchronous state, redux on the server is relatively simple to write, and there is no complexity. In fact, it can be written by passing the store directly to the Provider
  • server/index.js
import { Provider } from 'react-redux';
import { Route } from 'react-router-dom';

let domContent = renderToString(
  <Provider store={getServerStore()}>
    <StaticRouter context={context} location={req.path}>
      <>
        <Header/>
        <div className="container" style={{ marginTop: 70 }}>
            {
              routes.map(route => <Route {...route} />)
            }
          </div>
      </>
    </StaticRouter>
  </Provider>
);

2.4 interface services

  • /server/app.js, the cross domain has been completely opened here, and it will not be processed temporarily. Later, it needs to be modified and adjusted
const express = require('express');

let app = express();
const PORT = 8758;

app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "content-type");
  res.header("Access-Control-Allow-Methods", "DELETE,PUT,POST,GET,OPTIONS");
  next();
});

app.get('/api/getSchoolList', (req, res) => {
  let schoolList = [
    { id: 1, name: 'Animal University' },
    { id: 2, name: 'Botanical University' },
    { id: 3, name: 'Architecture University' },
    { id: 4, name: 'Fashion University' }
  ]
  return res.json({ schoolList });
});

app.listen(PORT, err => {
  if (err) {
    console.log(err);
  } else {
    console.log(`the server is running at http://localhost:${PORT}`);
  }
});

2.4 summary

  • Generally speaking, the synchronous redux and the client asynchronously acquire data, which is actually no different from the normal client rendering, so it is relatively simple
  • The complexity is asynchronous acquisition at the server side, which involves component methods, promise packaging, dehydration and water injection, etc. Let's put them in the next section

3. Split the code in server/index.js

  • Because we need to modify the code of server/index.js many times later, we first split the code into a render.js file, which is specially used for rendering, while the index.js file is only used for separate services
  • /server/index.js
import express from 'express';
import render from './render';

const app = express();
const PORT = 3000;

app.use(express.static('public'));

app.get('*', (req, res) => {
  render(req, res);
});

app.listen(PORT, err => {
  if (err) {
    console.log(err);
  } else {
    console.log(`Server is running at http://localhost:${PORT}`);
  }
});
  • /server/render.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter, Route, matchPath } from 'react-router-dom';
import { Provider } from 'react-redux';
import { getServerStore } from '../store';

import Header from './../components/Header/index';
import routes from '../routes';

export default (req, res) => {

  let context = {};

  let store = getServerStore();

  let domContent = renderToString(
      <Provider store={store}>
        <StaticRouter context={context} location={req.path}>
          <>
            <Header />
            <div className="container" style={{ marginTop: 70 }}>
              {
                routes.map(route => <Route {...route} />)
              }
            </div>
          </>
        </StaticRouter>
      </Provider>
    );
    let html = `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
  <link href="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/css/bootstrap.css" rel="stylesheet">
  <title>react-ssr</title>
</head>
<body>
<div id="root">${domContent}</div>
<script>
  window.context = {
    state: ${JSON.stringify(store.getState())}
  }
</script>
<script src="/client.js"></script>
</body>
</html>
`;

    res.send(html);
};

Related reading

Tags: Javascript React axios npm JSON

Posted on Fri, 08 Nov 2019 05:11:52 -0500 by jasonmills58