ahbeng/NUSMods

View on GitHub
website/src/views/components/map/withVenueLocations.tsx

Summary

Maintainability
A
0 mins
Test Coverage
import { ComponentType } from 'react';
import Loadable, { LoadingComponentProps } from 'react-loadable';

import { VenueLocationMap } from 'types/venues';
import { Subtract } from 'types/utils';
import LoadingSpinner from 'views/components/LoadingSpinner';
import ApiError from 'views/errors/ApiError';
import { getVenueLocations } from 'apis/github';

export type VenueLocations = {
  readonly venueLocations: VenueLocationMap;
};

export type ErrorProps = { error: unknown; retry: () => void };

export type WithVenueLocationsOptions = {
  Error?: ComponentType<ErrorProps>;
  Loading?: ComponentType;
};

const defaultErrorComponent = ({ retry }: ErrorProps) => <ApiError dataName="page" retry={retry} />;
const defaultLoadingComponent = () => <LoadingSpinner />;

/**
 * Higher order component that injects venueLocations into an async loaded
 * component. The component will only render when venueLocations is loaded.
 *
 * @param getComponent Function that returns a Promise resolving to the component
 * @param Error        Component shown when either the component or the data cannot be loaded
 *                     Defaults to <ApiError />
 * @param Loading      Component shown while the data is loading
 *                     Defaults to <LoadingSpinner />
 */
export default function withVenueLocations<Props extends VenueLocations>(
  getComponent: () => Promise<ComponentType<Props>>,
  {
    Error = defaultErrorComponent,
    Loading = defaultLoadingComponent,
  }: WithVenueLocationsOptions = {},
) {
  return Loadable.Map({
    loader: {
      Component: getComponent,
      venueLocations: getVenueLocations,
    },

    loading: (props: LoadingComponentProps) => {
      if (props.error) {
        return (
          <Error
            error={props.error}
            retry={() => {
              // Need to clear the memoized value first, otherwise the promise
              // will always resolve to the same error
              getVenueLocations.clear();
              props.retry();
            }}
          />
        );
      }

      if (props.pastDelay) {
        return <Loading />;
      }

      return null;
    },

    render({ Component, venueLocations }, props: Subtract<Props, VenueLocations>) {
      const propsWithVenueLocations = { venueLocations, ...props } as Props;
      return <Component {...propsWithVenueLocations} />;
    },
  });
}