Routing caching issues in single-page applications

Usually when we do a page back, the browser will keep track of the previous scroll position so that we don’t lose the previous browser record location every time we do a page back. However, in the increasingly popular SPA(Single Page Application), when we open the child page from the parent page, or enter the details page from the list page, if we go back to the page, we will find that the scroll record we browse before is gone, and the page is placed at the top. It’s like entering this page for the first time. This is because in the spa page url and routing in the container, when the path does not match with the page, the page components will be uninstalled, enter the page again, the entire life cycle of components will be completely to walk again, including some of the data request and rendering, so before rolling position and render the data content were completely reset.

Solution in VUE

The most thoughtful thing about vue.js is that it provides a lot of convenient apis for developers to consider many application scenarios. In VUE, if we want to cache routes, we can use the built-in keep-alive component directly. When keep-Alive wraps dynamic components, it caches inactive component instances rather than destroying them.

Built-in component Keep Alive

Keep-alive is a built-in component of vue.js. It is primarily used to preserve component state or avoid re-rendering.

The usage method is as follows:

<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>
Copy the code

The keep-alive component matches sub-components whose names are ‘a’ and ‘b’, and helps the component cache optimize the component so that it is not destroyed.

Realize the principle of

Take a brief look at the internal implementation code of the Keep-Alive component. See Vue GitHub for details

created () {
  this.cache = Object.create(null)
  this.keys = []
}
Copy the code

In the Created lifecycle, the object. create method is used to create a cache Object, which is used as a cache container to hold vNodes. Tip: Object.create(null) Creates objects that are purer without prototype chains

render () {
  const slot = this.$slots.default
  const vnode: VNode = getFirstComponentChild(slot)
  constcomponentOptions: ? VNodeComponentOptions = vnode && vnode.componentOptionsif (componentOptions) {
    // Check pattern Checks whether the match is a cache component based on the name passed in by include
    constname: ? string = getComponentName(componentOptions)const { include, exclude } = this
    if (
      // Not included with the current vnode(vDOM)(include && (! name || ! matches(include, name))) ||// excluded
    (exclude && name && matches(exclude, name))
    ) {
      return vnode
    }

    const { cache, keys } = this
    constkey: ? string = vnode.key ==null
      // same constructor may get registered as different local components
      // so cid alone is not enough (#3269)
      ? componentOptions.Ctor.cid + (componentOptions.tag ? ` : :${componentOptions.tag}` : ' ')
      : vnode.key
    if (cache[key]) {
      Vnode uses the component instance in the cache
      vnode.componentInstance = cache[key].componentInstance
      // make current key freshest  
      remove(keys, key)
      keys.push(key)
    } else {
      // Uncached instances are cached
      cache[key] = vnode
      keys.push(key)
      // prune oldest entry
      if (this.max && keys.length > parseInt(this.max)) {
        pruneCacheEntry(cache, keys[0], keys, this._vnode)
      }
    }

    vnode.data.keepAlive = true
  }
  return vnode || (slot && slot[0])}Copy the code

The above code is mainly used in the render function to determine whether the render is cached or not

The basic flow of vue keep-Alive internally is as follows:
  1. The inner child component is first retrieved by getFirstComponentChild
  2. Then get the name of the component and match it with the include and exclude attributes defined on the keep-alive component.
  3. If there is no match, the component is not cached and the vNode of the component is returned directly. (Vnode is a virtual DOM tree structure. Due to the large number of attributes in the native DOM, it consumes a lot of energy.
  4. If a match is found, check whether the instance has been cached in the cache object. If so, overwrite the cached componentInstance on the current VNode. Otherwise, store the VNode in the cache.

Solutions in React

React doesn’t provide a solution similar to Vue’s Keep-Alive, which means we may need to write some code or use some third-party modules to solve the problem.

In this issue on React project GitHub, developers and maintainers offer two solutions:

  • Cache data separately from components. For example, you can promote state to a parent component that will not be unloaded, or put it in a side cache like Redux. We are also developing a kind of API support (context) for this.
  • Do not uninstall the view you want to “keep active”, just usestyle={{display:'none'}}Properties to hide them.

1. Centralized status management restores snapshots

React uses centralized state management in Redux or MOBx to cache page data and scrollbar information.

componentDidMount() {
  const {app: {dataSoruce = [], scrollTop}, loadData} = this.props;
  if (dataSoruce.length) { // Determine if there is already a data source in redux
    // If there is data, the receipt will not be loaded, and only the scrolling state will be restored
    window.scrollTo(0, scrollTop);
  } else { // Request data source without data
    this.props.loadData(); // Action for the data request defined in redux
  }
}

handleClik = (a)= >{save the current scroll distance before clicking on the next pageconst scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
  const {saveScrollTop} = this.props;
  saveScrollTop(scrollTop);
}
Copy the code

First, we can define asynchronous actions for the page in Redux, putting the requested data into a centralized store (the specific use of redux is not covered in detail). In Sotre we can save the data source of the current page, scrollbar height, and other paging data that may be needed to help us recover.

In the componentDidMount life cycle, first determine if the data source has been loaded based on the corresponding field in the Store in redux. If the data has already been cached, the data source will not be requested, and only some scrollbar location information stored in the store will be recovered. If no data has been requested, use the asynchronous action defined in redux to request data and save the data in the Reducer into the store. In the Render function, we just need to read the data stored in the REdux.

To keep information about the state of the page that we want to cache, such as scroll bars, paging, and operation states, we can store this information in Redux’s Store during operations so that when we restore the page, the corresponding states can be read and restored.

2. Use the display attribute to switch the display and hide routing components

If you want to switch the display property to show and hide the routing component, first ensure that the routing component will not be unloaded when the URL changes. The Route component most used in the React-Router can match the page path with the path attribute defined by us and render the corresponding component, so as to keep the UI and URL changing synchronously.

First, take a brief look at the implementation of the Route component GitHub route.js

return (
  <RouterContext.Provider value={props}>{children && ! isEmptyChildren(children) ? Children: props. Match // props. Match attribute to determine whether to render the component? component ? React.createElement(component, props) : render ? render(props) : null : null}</RouterContext.Provider>
);
Copy the code

The above code appears in the return at the end of the key Render method

The Route component determines whether to render the component based on the Match property in the Props object. If a match is found, the component or render property passed on the Route component is used to render the component, otherwise null is returned.

We then trace back to the props object to find the definition of match:

const location = this.props.location || context.location;
const match = this.props.computedMatch
  ? this.props.computedMatch // <Switch> already computed the match for us
  : this.props.path
    ? matchPath(location.pathname, this.props)
    : context.match;

constprops = { ... context, location, match };Copy the code

The code above shows that match is first judged from the computedMatch attribute in the component’s this.props: If there is a computedMatch in this.props, then assign the defined computedMatch attribute to match. Otherwise, if this. The matchPath method is used to determine a match based on the current location. pathName.

ComputedMatch is not mentioned in the React Router Route component API documentation, but there is a line of comment in the source code

// <Switch> already computed the match for us
Copy the code

The comment says that the match has been calculated for us in the

component.

Let’s take a look at the Switch component:

The Switch component will only render the first

or

element matched by location as a child.

We open Switch component implementation source code:

let element, match; // Define the component element returned at the end, and the match match variable
  
  React.Children.forEach(this.props.children, child => {
    if (match == null && React.isValidElement(child)) { // If match has no content, enter the judgment
      element = child;
  
      const path = child.props.path || child.props.from;
  
      match = path  // This ternary expression assigns an object to match only after it is matched, otherwise match remains null
        ? matchPath(location.pathname, { ...child.props, path })
        : context.match;
    }
  });
  
  return match
    ? React.cloneElement(element, { location, computedMatch: match })
    : null;
Copy the code

The cloneElement method merges the append attributes to the Clone component element and returns the Clone component. Is equal to passing the new props property to the component and returning the new component.

MatchPath = matchPath = matchPath = matchPath = matchPath = matchPath = matchPath = matchPath = matchPath = matchPath = matchPath = matchPath = matchPath = matchPath = matchPath This method checks if the first parameter you pass in is pathName and the second props property parameter to match. Returns an object type containing the associated attribute if it matches, and null otherwise.

In the react.children. ForEach subelement method, the matchPath method determines whether the current pathname matches. If it does, it assigns a value to the defined match variable. Because the Switch component only renders the component that matches it the first time.

3. Implement a route cache component

We know that the Switch component only renders the children of the first match. The second solution would be to render all matched components and then toggle them with display blocks and None.

To encapsulate a RouteCache component, refer to the Switch component:

import React from 'react';
import PropTypes from 'prop-types';
import {matchPath} from 'react-router';
import {Route} from 'react-router-dom';

class RouteCache extends React.Component {

  static propTypes = {
    include: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.array
    ])
  };

  cache = {}; // Cache loaded components

  render() {
    const {children, include = []} = this.props;

    return React.Children.map(children, child => {
      if (React.isValidElement(child)) { // Verify that it is react Element
        const {path} = child.props;
        constmatch = matchPath(location.pathname, {... child.props, path});if (match && (include === true || include.includes(path))) {
          // If a match is found, the corresponding path computedMatch attribute is added to the cache object
          // Cache all components when include is true, and components when include is an array
          this.cache[path] = {computedMatch: match};
        }

        // Add a display attribute to computedMatch, which can be obtained from props. Match of the routing component
        const cloneProps = this.cache[path] && Object.assign(this.cache[path].computedMatch, {display: match ? 'block' : 'none'});

        return <div style={{display: match ? 'block' : 'none'}} >{React.cloneElement(child, {computedMatch: cloneProps})}</div>;
      }

      return null; }); }}/ / use
<RouteCache include={['/login'.'/home']} ><Route path="/login" component={Login} />
  <Route path="/home" component={App} />
</RouteCache>
Copy the code

After reading the source code, we know that the Route component determines whether to render the component based on its this.props.computedMatch.

We create a cache object inside the component and write the already matched component’s computedMatch properties to the cache object. This allows you to assign the computedMatch property to the component’s props by reading the value of the path in the cache object and using the React. CloneElement method, even when the URL no longer matches. This way, cached routing components will always be rendered and components will not be unloaded.

Because multiple routing components may be wrapped inside a component, use the react.children. Map method to loop back all contained child components.

In order to display the UI and route correctly, we can hide the mismatched components through the match attribute obtained by the current calculation, and only show the matched components. If you don’t want to layer div around the component, you can switch the display component inside the component using the display attribute in this.props. Match.

Set an include parameter API in the form of vue Keep Alive. Caches all internal child components when the argument is true and the corresponding path component when the argument is array.

Use effect





componentDidMount
display

4. In addition, some third-party component modules can also be used to practice the caching mechanism:

  1. react-keeper
  2. react-router-cache-route
  3. react-live-route