Advanced List in React - Build a powerful Component (Part III)

The last two parts of the tutorial series in React introduced two functionalities, a paginated list and an infinite scroll, by using higher order components. However, these functionalities were used exclusively. In one scenario you used a paginated list, where you manually fetched the data, then again in another scenario you used an infinite scroll, where the data was fetched automatically for you.

Now you could use both features exclusively. But what about using them in combination to give your user an improved user experience? You could use the infinite scroll as default behavior for your list. Your users will thank you, because they don’t have to fetch more list items by using the More button. Then, when your request runs into an error, you could use the More button as fallback. The user can manually try again to fetch data. That’s a great user experience and done already by applications like Twitter and Pocket.

Catching the Error in Local State

The goal is to give the user of your list component a possibility to jump in when an error occurs. First, you would have to track the error when a request fails. You will have to implement error handling in your React local state:

const applyUpdateResult = (result) => (prevState) => ({
  hits: [...prevState.hits, ...result.hits],
  page: result.page,
  isError: false,
  isLoading: false,
});

const applySetResult = (result) => (prevState) => ({
  hits: [...prevState.hits, ...result.hits],
  page: result.page,
  isError: false,
  isLoading: false,
});

const applySetError = (prevState) => ({
  isError: true,
  isLoading: false,
});

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      hits: [],
      page: null,
      isLoading: false,
      isError: false,
    };
  }

  fetchStories = (value, page) => {
    this.setState({ isLoading: true });
    fetch(getHackerNewsUrl(value, page))
      .catch(this.onSetError)
      .then(response => response.json())
      .then(result => this.onSetResult(result, page));
  }

  onSetError = () =>
    this.setState(applySetError);

  onSetResult = (result, page) =>
    page === 0
      ? this.setState(applySetResult(result))
      : this.setState(applyUpdateResult(result));

  render() {
    ...
  }
}

Basically when a request fails and you run into the catch block of your fetch function, you will store a simple boolean in your local state that indicates an error. When the request succeeds, you will keep set the isError property to false. You can use the introduced property in your components now.

class App extends React.Component {
  ...

  render() {
    return (
      <div className="page">
        <div className="interactions">
          <form type="submit" onSubmit={this.onInitialSearch}>
            <input type="text" ref={node => this.input = node} />
            <button type="submit">Search</button>
          </form>
        </div>

        <AdvancedList
          list={this.state.hits}
          isError={this.state.isError}
          isLoading={this.state.isLoading}
          page={this.state.page}
          onPaginatedSearch={this.onPaginatedSearch}
        />
      </div>
    );
  }
}

As you might have noticed, the enhanced List component got renamed to AdvancedList. How will it be composed? Basically it uses both functionalities, the manual fetch with the More button and automatic fetch with the infinite scroll, combined instead of exclusively.

Combine Higher Order Components

The composition of these functionalities would look like the following:

const AdvancedList = compose(
  withPaginated,
  withInfiniteScroll,
  withLoading,
)(List);

However, now both features would be used together without any prioritization. The goal would be to use the infinite scroll on default, but opt-in the More button when an error occurs. In addition, the More button should indicate the user that an error occurred and he or she can try again to fetch the sublist. The manual paginated fetch is the fallback when an error happens.

Let’s adjust the withPaginate higher order component to make it clear for the user that an error happened and that he or she can try it again manually by clicking the button.

const withPaginated = (Component) => (props) =>
  <div>
    <Component {...props} />

    <div className="interactions">
      {
        (props.page !== null && !props.isLoading && props.isError) &&
        <div>
          <div>
            Something went wrong...
          </div>
          <button
            type="button"
            onClick={props.onPaginatedSearch}
          >
            Try Again
          </button>
        </div>
      }
    </div>
  </div>

In addition, the infinite scroll higher order component should be inactive when there is an error.

const withInfiniteScroll = (Component) =>
  class WithInfiniteScroll extends React.Component {
    ...

    onScroll = () => {
      if (
        (window.innerHeight + window.scrollY) >= (document.body.offsetHeight - 500) &&
        this.props.list.length &&
        !this.props.isLoading &&
        !this.props.isError
      ) {
        this.props.onPaginatedSearch();
      }
    }

    render() {
      return <Component {...this.props} />;
    }
  }

Now you can try the functionality in the browser. First, you make an initial search to trigger a request to the Hacker News API. Afterward you can scroll down several times to verify the working infinite scroll functionality. In the network tab of your developer console, you can simulate that your browser tab is offline. When you toggle it to offline and scroll again, you will see that the More button appears. That’s your fallback because the request to the Hacker News API failed. The user of your application gets a great user experience, because he or she knows what happened and can try it again. You can click the More button, but it will fails as long as your toggled the browser tab as offline. Once you toggle it to online again, the manual fetch by using the More button should work. The next time you scroll down the default behavior, the infinite scroll, should work again.

Configure Higher Order Components

There is one last optimization left. Unfortunately both HOCs that provide the infinite scroll and paginated list behavior are dependent on each other. Both use props that are not really used in the higher order component itself. These props are unnecessary dependencies. For instance, the infinite scroll shouldn’t know about the isError property. It would be good to make the components unaware of their conditions. These conditions could be extracted as configurations for the higher order components. Once again, if you are not sure about a configuration in a higher order component, you can read the gentle introduction to higher order components article.

Let’s extract the conditions as configuration for each higher order components. First, give your higher order components a conditionFn function as configuration.

const withLoading = (conditionFn) => (Component) => (props) =>
  <div>
    <Component {...props} />

    <div className="interactions">
      {conditionFn(props) && <span>Loading...</span>}
    </div>
  </div>

const withPaginated = (conditionFn) => (Component) => (props) =>
  <div>
    <Component {...props} />

    <div className="interactions">
      {
        conditionFn(props) &&
        <div>
          <div>
            Something went wrong...
          </div>
          <button
            type="button"
            onClick={props.onPaginatedSearch}
          >
            Try Again
          </button>
        </div>
      }
    </div>
  </div>

const withInfiniteScroll = (conditionFn) => (Component) =>
  class WithInfiniteScroll extends React.Component {
    componentDidMount() {
      window.addEventListener('scroll', this.onScroll, false);
    }

    componentWillUnmount() {
      window.removeEventListener('scroll', this.onScroll, false);
    }

    onScroll = () =>
      conditionFn(this.props) && this.props.onPaginatedSearch();

    render() {
      return <Component {...this.props} />;
    }
  }

Second, define these conditionFn functions outside of your higher order components. Thus, each higher order component can define flexible conditions.

const paginatedCondition = props =>
  props.page !== null && !props.isLoading && props.isError;

const infiniteScrollCondition = props =>
  (window.innerHeight + window.scrollY) >= (document.body.offsetHeight - 500)
  && props.list.length
  && !props.isLoading
  && !props.isError;

const loadingCondition = props =>
  props.isLoading;

const AdvancedList = compose(
  withPaginated(paginatedCondition),
  withInfiniteScroll(infiniteScrollCondition),
  withLoading(loadingCondition),
)(List);

The condiditions will get evaluated in the higher order components themselves. That’s it.


In the last three parts of this React tutorial series you have learned to build a complex list component by using React’s higher order components. The list component itself stays simple. It only displays a list. But it can be composed into useful higher order components to opt-in functionalities. By extracting the conditions from the higher order components and using them as configuration, you have the control about which component is used first as default and which should be used as opt-in feature. The full example application can be found in this repository. If you are keen to explore more about these functionalities when working with lists in the local state of React, you can read the Road to learn React to learn about caching in React.

Build a Hacker News App along the way. No setup configuration. No tooling. No Redux. Plain React in 190+ pages of learning material. Learn React like 13.000+ readers.

Get the Book
comments powered by Disqus

Never miss an article about web development and self-growth.

Take Part

Join 7800+ Developers

Learn Web Development with JavaScript

Tips and Tricks

Access Tutorials, eBooks and Courses

Personal Development as a Software Engineer