Home Reference Source

src/components/controls/withFetchData.js

import React from 'react';
import PropTypes from 'prop-types';
import pathToRegexp from 'path-to-regexp';
import { fetchInputSelectData } from '../../core/api';
import cachingStore from '../../utils/cacher';
import { connect } from 'react-redux';
import { get, isArray, has, unionBy } from 'lodash';
import { addAlert, removeAlerts } from '../../actions/alerts';
import { getParams } from '../../utils/compileUrl';

/**
 * HOC для работы с данными
 * @param WrappedComponent - оборачиваемый компонент
 * @param apiCaller - promise для вызова апи
 */

function withFetchData(WrappedComponent, apiCaller = fetchInputSelectData) {
  class WithFetchData extends React.Component {
    constructor(props) {
      super(props);

      this.state = {
        data: [],
        isLoading: false,
        count: 0,
        size: props.size,
        page: 1,
        hasError: false,
      };

      this._fetchData = this._fetchData.bind(this);
      this._mapping = this._mapping.bind(this);
      this._findResponseInCache = this._findResponseInCache.bind(this);
      this._fetchDataProvider = this._fetchDataProvider.bind(this);
      this._addAlertMessage = this._addAlertMessage.bind(this);
      this._setErrorMessage = this._setErrorMessage.bind(this);
      this._setResponseToData = this._setResponseToData.bind(this);
    }

    static getDerivedStateFromProps(nextProps) {
      if (nextProps.data && nextProps.data.length) {
        return {
          data: nextProps.data,
        };
      }

      return null;
    }

    /**
     * Взятие данных для запроса по link или по контексту.
     * @param mappingConfig
     * @returns {{}}
     * @private
     */
    _mapping(mappingConfig) {
      return getParams(mappingConfig, this.context.store.getState());
    }

    /**
     * Поиск в кеше запроса
     * @param params
     * @returns {*}
     * @private
     */
    _findResponseInCache(params) {
      const { caching } = this.props;
      if (caching && cachingStore.find(params)) {
        return cachingStore.find(params);
      }
      return false;
    }

    /**
     * Вывод сообщения
     * @param messages
     * @private
     */
    _addAlertMessage(messages) {
      const { hasError } = this.state;
      const { addAlert, removeAlerts } = this.props;

      !hasError && this.setState({ hasError: true });

      removeAlerts();
      isArray(messages)
        ? messages.map(m => addAlert({ ...m, closeButton: false }))
        : addAlert({ ...messages, closeButton: false });
    }

    /**
     * Вывод сообщения с ошибкой
     * @param response
     * @private
     */
    async _setErrorMessage({ response }) {
      let errorMessage = null;
      if (response) {
        errorMessage = await response.json();
      } else {
        errorMessage = arguments[0].body;
      }
      const messages = get(errorMessage, 'meta.alert.messages', false);
      messages && this._addAlertMessage(messages);
    }

    /**
     * Взять данные с сервера с помощью dataProvider
     * @param pathMapping
     * @param queryMapping
     * @param url
     * @param extraParams
     * @returns {Promise<void>}
     * @private
     */
    async _fetchDataProvider(
      { pathMapping, queryMapping, url },
      extraParams = {}
    ) {
      const pathParams = this._mapping(pathMapping);
      const queryParams = this._mapping(queryMapping);
      const basePath = pathToRegexp.compile(url)(pathParams);
      let response = this._findResponseInCache({
        basePath,
        queryParams,
        extraParams,
      });

      if (!response) {
        response = await apiCaller({ ...queryParams, ...extraParams }, null, {
          basePath,
        });
        cachingStore.add({ basePath, queryParams, extraParams }, response);
      }

      return response;
    }

    /**
     *  Обновить данные если запрос успешен
     * @param list
     * @param count
     * @param size
     * @param page
     * @param merge
     * @private
     */
    _setResponseToData({ list, count, size, page }, merge = false) {
      const { valueFieldId } = this.props;
      this.setState({
        data: merge
          ? unionBy(this.state.data, list, valueFieldId || 'id')
          : list,
        isLoading: false,
        count,
        size,
        page,
      });
    }

    /**
     * Получает данные с сервера
     * @param extraParams - параметры запроса
     * @param concat - флаг объединения данных
     * @returns {Promise<void>}
     * @private
     */

    async _fetchData(extraParams = {}, merge = false) {
      const { dataProvider, removeAlerts } = this.props;
      const { hasError, data } = this.state;
      if (!dataProvider) return;

      this.setState({ loading: true });
      try {
        if (!merge && data) this.setState({ data: [] });
        const response = await this._fetchDataProvider(
          dataProvider,
          extraParams
        );
        if (has(response, 'message')) this._addAlertMessage(response.message);
        this._setResponseToData(response, merge);
        hasError && removeAlerts();
      } catch (err) {
        await this._setErrorMessage(err);
      } finally {
        this.setState({ loading: false });
      }
    }

    /**
     * Рендер
     */

    render() {
      return (
        <WrappedComponent
          {...this.props}
          {...this.state}
          _fetchData={this._fetchData}
          ref={this.props.setRef}
        />
      );
    }
  }

  WithFetchData.propTypes = {
    caching: PropTypes.bool,
    size: PropTypes.number,
  };

  WithFetchData.contextTypes = { store: PropTypes.object };

  WithFetchData.defaultProps = {
    caching: false,
    size: 10,
  };

  const mapDispatchToProps = (dispatch, ownProps) => ({
    addAlert: message =>
      dispatch(addAlert(ownProps.form + '.' + ownProps.labelFieldId, message)),
    removeAlerts: () =>
      dispatch(removeAlerts(ownProps.form + '.' + ownProps.labelFieldId)),
  });

  return connect(
    null,
    mapDispatchToProps
  )(WithFetchData);
}

export default withFetchData;