Search code examples
javascriptreactjstypescriptabstraction

How to refactor three components, which asynchronously load and display data into one?


I have the following TypeScript code. I simplified/remove as much as I could.

interface DataPullingPageState
{
  loading: boolean;
  displayedEntries: string[];
};

export class EntriesPageOne extends React.Component<{}, DataPullingPageState>
{
  constructor(props: any)
  {
    super(props);

    this.state = { loading: false, displayedEntries: [] };
  }

  async componentDidMount()
  {
    this.setState({ loading: true });

    const entries = await api.loadAll();

    this.setState({ loading: false, displayedEntries: entries });
  }

  render()
  {
    if (this.state.loading)
    {
      return <div>loading</div>;
    }
    else if (this.state.displayedEntries.length === 0)
    {
      return <div>nothing found</div>;
    }
    else
    {
      return this.state.displayedEntries.map((entry, i) => <div key={i}>{entry}</div>);
    }
  }
}

export class EntriesPageTwo extends React.Component<{}, DataPullingPageState>
{
  constructor(props: any)
  {
    super(props);

    this.state = { loading: false, displayedEntries: [] };
  }

  async componentDidMount()
  {
    this.setState({ loading: true });

    const param = "my param";
    const entries = await api.loadByStringParam(param);

    this.setState({ loading: false, displayedEntries: entries });
  }

  render()
  {
    if (this.state.loading)
    {
      return <div>loading</div>;
    }
    else if (this.state.displayedEntries.length === 0)
    {
      return <div>nothing found</div>;
    }
    else
    {
      return this.state.displayedEntries.map((entry, i) => <div key={i}>{entry}</div>);
    }
  }
}

export class EntriesPageThree extends React.Component<{}, DataPullingPageState>
{
  constructor(props: any)
  {
    super(props);

    this.state = { loading: false, displayedEntries: [] };
  }

  async componentDidMount()
  {
    this.setState({ loading: true });

    const param = 123;
    const entries = await api.loadByNumberParam(param);

    this.setState({ loading: false, displayedEntries: entries });
  }

  render()
  {
    if (this.state.loading)
    {
      return <div>loading</div>;
    }
    else if (this.state.displayedEntries.length === 0)
    {
      return <div>nothing found</div>;
    }
    else
    {
      return this.state.displayedEntries.map((entry, i) => <div key={i}>{entry}</div>);
    }
  }
}

As you can see it's three different components that all display the same but have three different ways of loading it.

I'd like to know how I can make only one component out of those three. I've already heard about HoC but don't know if they suit my case.


Solution

  • Yes you can HoC let's simplify your code a bit:

    HoC Method

    class EntriesPage extends React.Component {
      // you don't need state for loading
      render() {
        const { loading, entries } = this.props
      }
    }
    EntriesPage.defaultProps = { loading: true, entries: [] }
    
    const withEntries = (apiCall) => (Page) => {
      return async (props) => {
         const entries = await apiCall()
         <Page {...props} loading={false} entries={entries} />
      }
    }
    

    Now you can compose first page like this

    // PageOne
    export default withEntries(api.loadAll)(EntriesPage)
    // PageTwo
    export default withEntries(() => api.loadByStringParam('param'))(EntriesPage)
    // PageThree
    export default withEntries(() => api.loadByNumberParam(123))(EntriesPage)
    

    This will create HoC which accepts dynamic fetching method and pass the result as prop to the final component. Hope this helps

    Hoc method with param as prop

    You can even expose params to the final component by changing it to something like this

    const withEntries = (apiCall) => (Page) => {
      return async (props) => {
         const { fetchParam, ...rest } = props
         const entries = await apiCall(fetchParam)
         <Page {...rest} loading={false} entries={entries} />
      }
    }
    
    // EntriesPageComposed.tsx
    export default withEntries(api.loadByStringParam)(EntriesPage)
    
    <EntriesPageComposed fetchParams={123} />
    

    Loader component

    Or you can even make it completely isolated without HoC and pass everything as prop and make "data loader" component, which is quite common pattern in React apps, which will act only as loader for preparing next props.

    const ComposedComponent = async (props) => {
      const { fetchMethod, fetchParam, ...rest } = props
      const entries = await fetchMethod(fetchParam)
    
      return (
        <EntriesPage {...rest} loading={false} entries={entries} />
      )
    }
    
    
    <ComposedComponent fetchMethod={api.loadByStringParam} fetchParam={'param'} />
    

    In this way you have initial implementation isolated and you can add new fetch methods on the fly just by passing a prop.