pacificclimate/climate-explorer-frontend

View on GitHub
src/HOCs/withAsyncData.js

Summary

Maintainability
A
1 hr
Test Coverage
// Higher-order function returning a Higher-Order Component (HOC) that injects
// asynchronously fetched data into a component.
//
// (See https://reactjs.org/docs/higher-order-components.html#convention-maximizing-composability
// for why this HOC is separated into two parts like this.)
//
// To manage asynchronous data fetching, this component follows React best
// practice:
// https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html#fetching-external-data-when-props-change
// This code is pretty much a straight-over port of that example.
//
// Data fetching occurs when the value of `this.props[controlProp]` changes.
// This is simpler, but adequate, special case of the general case that could
// be handled by passing in a function that compares all relevant props.
//
// The fetched data is injected into the base component through a prop passed
// to it named by `dataProp`.

import React from "react";
import Loader from "react-loader";

export default function withAsyncData(
  loadAsyncData, // Async data fetcher. Returns a promise.
  controlProp, // Name of prop that controls data fetching
  dataProp, // Name of prop to pass data to base component through
) {
  return function (BaseComponent) {
    return class extends React.Component {
      state = {
        externalData: null,
      };

      static getDerivedStateFromProps(props, state) {
        // Store previous control prop in state so we can compare when props
        // change.
        // Clear out previously-loaded data so we don't deliver stale data.
        if (props[controlProp] !== state.prevControl) {
          return {
            externalData: null,
            prevControl: props[controlProp],
          };
        }

        // No state update necessary
        return null;
      }

      componentDidMount() {
        this._loadAsyncData(this.props[controlProp]);
      }

      componentDidUpdate(prevProps, prevState) {
        if (this.state.externalData === null) {
          this._loadAsyncData(this.props[controlProp]);
        }
      }

      componentWillUnmount() {
        if (this._asyncRequest && this._asyncRequest.cancel) {
          this._asyncRequest.cancel();
        }
      }

      _loadAsyncData(...args) {
        this._asyncRequest = loadAsyncData(...args).then((externalData) => {
          this._asyncRequest = null;
          this.setState({ externalData });
        });
      }

      render() {
        if (this.state.externalData === null) {
          return <Loader />;
        }
        return (
          <BaseComponent
            {...{ [dataProp]: this.state.externalData }}
            {...this.props}
          />
        );
      }
    };
  };
}