1 what is the LazyLoad

LazyLoad, in Chinese, is lazy loading or lazy loading. A variable starts loading its own content only when it is called. This way you can avoid the experience of taking too long for the first screen to load. LazyLoad is often used in daily development. Lazy in React and asynchronous components in Vue2 :()=>import(‘./SomeComponent’) are used to implement lazy loading of components. Today’s article discusses implementing lazy loading of non-basic types of data in Redux’s State.

Reading this article, you will:

  1. How to useredux-thunkTo realize data delay loading
  2. How to useredux-sagaTo realize data delay loading

These two methods, applied to projects, can greatly reduce the number and number of times we write dispatch statements repeatedly. To reduce the complexity of the project.

2 Redux StoreThe benefits of using lazy loading in

It is known that Redux State stores public variables, and some public variables are obtained asynchronously. If a component (discussed here with the React component) requires public variables such as group groups in its interaction, it is necessary to ensure that these public variables are loaded before the component interacts.

The Redux State change is triggered by the Dispatch Action. We typically write the Dispatch Action logic in two places:

  1. Write in the component’s lifecycle (useEffect or componentDidMount). There was a drawback: there were multiple components that used a common variable like groups, so each component would have to distribute the corresponding action in the same lifecycle, and multiple parts of the same logic would make our project code very bloated.

  2. Write it in the entry file. This makes up for the disadvantage of the first point above, but it also has a disadvantage: too many public variables can lead to too many requests and lead to a long first screen load time (after all, browsers have a limit on the maximum number of concurrent requests for the same domain name).

This can be avoided when lazy loading is used and the logic of lazy loading is written in redux-related operations.

Also, both point 1 and point 2 face a synchronous update problem, namely, there is no guarantee that the State in Redux State is up to date. Let’s set up a scenario: in a product website, there is a variable tags in Redux State that stores the object type of the tags. The tags need to be retrieved from the back end, and each item will be attached to one of the tags in the tags. Tags will change with the new tags of other merchants. Therefore, in the case that the tags in Redux State cannot be consistent with those in the back-end database, there will be an incomplete display of the tags of the goods when you browse the new goods.

In view of the above problems, some projects will use polling or Websocket communication to ensure data consistency, but such writing will increase the complexity of the project.

The lazy loading method can also solve the problem of synchronous update.

3 Implementation idea of lazy loading

Next, let’s assume that on an internal company website (similar to a work order management system), we need to query for information about the developer Users, and each developer belongs to a group. Recording these groups is a variable group of object type stored in Redux State. The data relationship is as follows:

  • users: Array<{id:string, group_id:string,name:string}>

    Users refers to the list of acquired developers, which is an array containing an object with three attributes: id, group_id, and name. Where id indicates the unique ID of the developer, group_id indicates the ID of the group, and name indicates the name of the developer.

  • Groups: {id: name}

    Groups indicates the group information and is an object. The key ID is the unique ID of the group, and the value name is the name of the group.

Group_id is used to read group information from groups, which then causes groups to load data asynchronously to update themselves. The effect is as follows:

3.1 How do I capture read behavior

We can use proxies in ES6 to capture read behavior. The known Proxy initialization modes are as follows:

const p = new Proxy(target, handler)

The parameters required for instantiation are as follows:

  • target:ProxyThe target of the wrapper must be a non-basic type of data, such as an array, function, or object.
  • handler: an object that defines read proxy functions that trigger the execution of read and write operations.

We take the grouped data (groups) as the target, define some properties in the handler, and instantiate the Proxy object groupProxy as parameters to the Proxy constructor. The groupProxy is assigned to the Groups variable in Redux state. As shown below:

Let’s focus on what attributes in handler help us capture those common read behaviors. Note that we are only concerned with capturing the read behavior and not the write behavior.

  1. handler.has: captureinBehavior, for example"0" in groups.
  2. handler.get: captures read behavior, for examplegroups["0"],groups.hasOwnProperty("0").
  3. handler.ownKeysCapture:Object.keysBehavior.

Values and object. entries trigger handler. OwnKeys and handler. Get.

To sum up, when we read groups in Redux State, it is actually a Proxy instance, which triggers the execution of functions defined in handler during the read operation.

3.2 What to do after capturing the read behavior

After capturing read behavior, consider two cases:

  1. Redux StatethegroupsThere is nouser.group_idThe corresponding group: that is, thegroupsNo load or data inconsistent with background. So there are two things to do:
  • The back end makes a request to getgroups. New ones are generated after data is retrievedgroupProxyreplaceRedux Statethegroups.Redux StateIs triggered by changes inReactComponent rerendering (react-reduxIn the libraryconnectFunction will makeReactComponent in the injectedStateVariables are updated when re-rendering), again during rendering toRedux StatethegroupsWhen reading data, it is already availableuser.group_idThe corresponding grouping will result in the second point below.
  • Returns a temporary value to the component.
  1. If the groups of Redux State contain a group corresponding to user.group_id: The group is returned directly.

The above process can be combined with the following flow chart:

4 Implement lazy-load with redux-thunk

In this demand, the back-end has two interfaces, one is the request of the users interface (http://localhost:8888/users), another is the request groups interface (http://localhost:8888/groups). The code on the back end looks like this:

var express = require('express')
var app = express()

/ / users data
const USERS = [
  {
    id:'0'.name:Users' A '.group_id:'0'
  },
  {
    id:'1'.name:Users' B '.group_id:'0'
  },
  {
    id:'2'.name:'the user C'.group_id:'1'}]/ / groups data
const GROUPS={
  '0':Group A ' '.'1':'group C'
}

const PORT=8888

// Use middleware to solve the same origin policy problem in browsers
app.use(function(req,res,next){
  // The response header sets the access-Control-allow-Origin field
  res.header("Access-Control-Allow-Origin"."*");
  next()
})

app.get('/users'.function (req, res) {
  res.send({users:USERS})
})

app.get('/groups'.function (req, res) {
  // The delay is set to 1s so that the load effect of groups in Users can be clearly seen
  setTimeout(() = > {
    res.send({groups:GROUPS})
  }, 1000);
})

app.listen(PORT)
Copy the code

Let’s try implementing the above logic with redux-thunk. First, show the request function

export const fetchUsers = () = >{
  return fetch('http://localhost:8888/users').then(res= >res.json())
}

const loading = false
export const fetchGroups = () = >{
  return fetch('http://localhost:8888/groups').then(res= >res.json())
}
Copy the code

Let’s take a look at store writing. First, take a look at the contents of the action:

action

import {fetchGroups} from '.. /.. /apis'

// Set groups in Redux State
export const SET_GROUPS=(groups) = >({
  type:'SET_GROUPS',
  groups
})

// Generate groupProxy and distribute SET_GROUPS
export const MAKE_GROUPS_PROXY = (groups) = >(dispatch) = >{
  const groupProxy = new Proxy(groups,{
    get(target, property){
      /** * Check the type and value of the property, * because when opening Chrome redux-devTools for debugging, Redux-devtools calls * Groups constructor and Symbol(symbol.toStringTag) to listen for updates. We don't need to handle calls to * properties that are on the prototype chain. So check the type of the property to see if group_id */ is passed in
      if(! (typeof property==='string'&&/\d+/.test(property))) return target[property]
      /** * If the group does not exist in the propped object target, return "loading" as a temporary value and issue REQUEST_GROUPS() * to trigger groups to synchronize data on the back end */
      if(! (propertyin target)){
        dispatch(REQUEST_GROUPS())
        return 'Loading'
      }
      // If the group exists in the propped object target, the value is returned directly
      return target[property]
    }
  })
  dispatch(SET_GROUPS(groupProxy))
}

// Used to prevent multiple reads from triggering REQUEST_GROUPS to execute multiple times
let loading = false
// Request groups and issue MAKE_GROUPS_PROXY
export const REQUEST_GROUPS = () = >async (dispatch) => { 
  if(loading) return
  loading = true
  const {groups} = await fetchGroups()
  loading = false
  dispatch(MAKE_GROUPS_PROXY(groups))
}
Copy the code

Let’s focus on MAKE_GROUPS_PROXY. It is a pure function, but it is written as asynchronous Action Creator** in Redux-thunk. This is because store.dispatch is required to distribute SET_GROUPS generated actions. The distribution flow of Action Creator above is as follows:

Then look at the reducer code:

const reducer = (state,action) = >{
  switch (action.type) {
    case 'SET_GROUPS':
      return {groups:action.groups}
    default:
      return state
  }
}

export default reducer
Copy the code

Finally, look at the logic that generates the store:

import { createStore,applyMiddleware } from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk'
import {MAKE_GROUPS_PROXY} from './action'

const store = createStore(reducer,{groups:{}}, applyMiddleware(thunk))
/** * issue MAKE_GROUPS_PROXY({}) to update groups in State, * groups was set to a pure object during store initialization, instead of a proxy instance */
store.dispatch(MAKE_GROUPS_PROXY({}))

export default store
Copy the code

Finally, show the main page logic: app.jsx

import React, { useState } from "react";
import { connect } from "react-redux";
import { Table, Button, Space } from "antd";
import {fetchUsers} from '.. /apis'
import {LoadingOutlined  } from '@ant-design/icons'

const App = (props) = > {
  const {groups} = props
  const [users, setUsers] = useState([]);

  const getUsers = async() = > {const {users} = await fetchUsers()
    setUsers(users)
  }

  const columns = [
    {
      title: "Name".dataIndex: "name".align: "center".width: 100}, {title: "Group".dataIndex: "group_id".align: "center".width: 100.render:(group_id) = >groups[group_id]==='Loading'?<LoadingOutlined />:groups[group_id]
    },
  ];
  return (
    <Space direction="vertical" style={{ margin: 12}} >
      <Button type="primary" onClick={getUsers}>The query</Button>
      <Table
        columns={columns}
        dataSource={users}
        bordered
        title={()= >} rowKey="id" ></Table>
    </Space>
  );
};

const mapStateToProps = ({groups}) = > ({
  groups
})

export default connect(mapStateToProps)(App);
Copy the code

The following results can be achieved:

The project address

5 Implement lazy-load with redux-saga

Of course, some projects use Redux-Saga instead of Redux-Thunk. Therefore, lazy-load is also provided under redux-saga.

In redux-Saga, however, all impure operations (that is, functions that are not pure) are treated as saga. In the makeGroupProxy (the proxy instance that generates the group) method, we need to make the get property in the handler trigger the saga execution of the response if the read fails. We know that the external triggering of saga execution usually has an action corresponding to dispatch, and we can’t get store.dispatch inside makeGroupProxy like redux-Thunk. So, is there another way we can trigger saga execution externally? The answer is: eventChannel.

I wrote a previous article explaining the use of eventChannel, but I won’t repeat it here. The lazy load implementation code for the Redux-Saga version is as follows:

In the redux-Thunk code above, we no longer need the action code, but instead create a new saga and start writing in it:

saga

import { eventChannel, buffers } from 'redux-saga';
import {fetchGroups} from '.. /.. /apis'
import {call,take,put} from 'redux-saga/effects'

// trigger Is used to trigger saga, which is called in the handler during Proxy instantiation
let trigger = null

export const makeGroupProxy = (target={}) = >{
  return new Proxy(target, {
    get: (target, property) = > {
      if(! (typeof property==='string'&&/\d+/.test(property))) return target[property]
      if(! (propertyin target)) {
        // If trigger is a function type, the call triggers the saga execution, which triggers the groups update
        if (trigger instanceof Function) {
          trigger({});
        }
        return 'Loading';
      }
      returntarget[property]; }}); }// Used to generate eventChannel
const makeRefreshGroupChannel = () = > {
  return eventChannel((emitter) = > {
    // Emitter is given to trigger
    trigger = emitter;
    return () = > {};
    /** Buffers. Dropping (0) indicates that the eventChannel does not allow caching external event sources that have not yet been processed. Emitter is being called many times and request groups are being repeated
  }, buffers.dropping(0));
};

export function* watchGroupSaga(){
  // Generate the eventChannel channel and listen
  const chan = yield call(makeRefreshGroupChannel);
  try {
    // When trigger is called, the while statement is entered
    while (yield take(chan)) {
      const {groups} = yield call(fetchGroups);
      if (groups) {
        yield put({
          type: 'SET_GROUPS'.// Generate groupProxy and update it to groups in Redux State
          groups: makeGroupProxy(groups), }); }}}finally {
    console.warn('watchGroup end.'); }}Copy the code

Finally, make logical changes to generate store:

import { createStore,applyMiddleware } from 'redux'
import reducer from './reducer'
import createSagaMiddleware from "redux-saga";
import {makeGroupProxy,watchGroupSaga} from './saga'

const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer,{groups:makeGroupProxy({})}, applyMiddleware(sagaMiddleware))
// Execute watchGroupSaga to enable listening on eventChannel
sagaMiddleware.run(watchGroupSaga);

export default store
Copy the code

The rest of the code is the same as redux-thunk, which implements the lazy loading of groups in the example above.

The project address

6 develop

In fact, there are many ways to use Proxy, I wrote a year ago to use Proxy to implement lazy loading of large third-party libraries, you can have a look.

7 afterword.

The following articles will be combined with the actual experience of the project to write, what do not understand welcome to comment on the message oh.