tim@kolberger.eu
“person standing on top of mountain” by Alessandro Erbetta on Unsplash

Creating a reusable React Query component for your REST API

When implementing a new UI component I always start with the layouting and mock the server communication by providing defaultProps and empty click listeners for user interactions. After completing the UI components I replace the mocked functions and props with the real thing.

I reinvented the wheel over and over again in every component which consumes data from a server. Using and configuring fetch with HTTP headers, deserialization logic and handling success, error and loading states add many and often duplicate lines to your codebase.

Wouldn’t it be nice to outsource the communication logic to a reusable component?

Just do it.

import React, { Fragment } from 'react';
import { Query } from './Query';
import { Loading, ErrorMessage, EndpointList } from './common';

export const BasicQuery = () => (
  <Fragment>
    <h2>BasicQuery</h2>
    <Query url="https://api.github.com">
      {({ state: { data, loading, error }, actions }) => {
        if (loading) {
          return <Loading />;
        }

        if (error) {
          return <ErrorMessage error={error} />;
        }

        if (data) {
          return (
            <React.Fragment>
              <button onClick={actions.fetch}>Reload</button>
              <EndpointList endpoints={data} />
            </React.Fragment>
          );
        }

        return null;
      }}
    </Query>
  </Fragment>
);

Working on a React application which consumes an API you have to deal with a bunch of difficulties. For every request you need to handle a loading, error and success state.

Integrating this functionality in an existing component will increase the complexity of it and does not leverage the component based approach of React.

Computer Science isn’t a new thing anymore and we are rediscovering some rules and tools from the old days. One of them is the rule of separation of concerns.

Write programs that do one thing and do it well

Doug McIlroy

Let’s transfer this quote by the inventor of the Unix pipe to React components, where a component in a React application is the corresponding program in a unix system. Provide props to control the behavior of a component and use an universal interface for your output. In JavaScript our universal interface is the function type — this leads us to Composite Components.

Provide reusable functionality with Composite Components

The composite component pattern is getting promoted by Kent C. Dodds which leverages another design principle in addition to the one-thing-principle: the inversion of control, which shifts the non-core functionality to another component.

Using a Query component gives you the ability to fetch a url — not no less. The decision which jsx-elements to render, based on the query result, is shifted to the using side. The Query component has no strong coupling to any component, not to the url nor any other prerequisite.

It is fully customizable wherever it is used. You can provide a custom deserialize function to deal with different response types like json, text or xml, check for the response status codes and alter the default behavior in place. A state reducer lets you intercept the state updates to change the Query component behavior based on the response results.

The Query component is a Composite Component and additionally renders a React Context Provider which wraps its children. This enables the using side to use Compound Components. Let’s see this in action:

import React, { Fragment } from 'react';
import { Query } from './Query';
import { Loading, ErrorMessage, SearchInput } from './common';

export const DynamicUrlQuery = () => (
  <Query
    options={{
      headers: { accept: 'application/json' },
    }}
    disableInitialFetch
  >
    {({ state: { data, loading, error }, actions }) => {
      const handleSearch = value =>
        actions.fetch({
          url:
            'https://api.github.com/search/users?q=' +
            encodeURIComponent(value),
          method: 'GET', // optional
        });

      return (
        <Fragment>
          <h2>DynamicUrlQuery</h2>
          <SearchInput onSearch={handleSearch} />
          {loading && <Loading />}
          {error && <ErrorMessage error={error} />}
          <pre>{JSON.stringify(data, null, 2)}</pre>
        </Fragment>
      );
    }}
  </Query>
);

If you have dipped your toes into the GraphQL universe you may have seen one of the Query, Mutation or Subscription components provided by the awesome react-apollo library. These components provide a straight forward API for integrating server communication logic in your React components — even for REST APIs with apollo-link-rest. I am totally into good developer experience when it comes to coding. But there are situations where you do not want to pull in an extra library as dependency. So let’s try to recreate a comparable developer experience on our own for a REST API.

Let’s take a look at a query component which passes additional information, like the loading and error states of the request, to the consuming child component.

import React, { Component, createContext } from 'react';

const initialState = {
  data: null,
  loading: false,
  error: null,
};

const initialQueryContextValue = {
  state: initialState,
  actions: {},
};

const QueryContext = createContext(initialQueryContextValue);

export class Query extends Component {
  static Consumer = QueryContext.Consumer;
  static defaultProps = {
    fetch,
    disableInitialFetch: false,
    stateReducer: (update, state, props) => update,
    deserialize: async res => res.json(),
  };

  state = initialState;

  setReducedState = update => {
    const { stateReducer } = this.props;
    this.setState(state => stateReducer(update, state, this.props));
  };

  request = async optionsPart => {
    const { fetch, url: propUrl, options, deserialize } = this.props;

    this.setReducedState({ loading: true });

    // use the url from the request argument or fallback to the url from props
    let url = (optionsPart && optionsPart.url) || propUrl;
    let fetchOptions = options;
    if (optionsPart) {
      // strip the url key from the fetch options if it is provided
      const { url, ...restOptions } = optionsPart;
      fetchOptions = { ...options, ...restOptions };
    }

    try {
      const res = await fetch(url, fetchOptions);
      const data = await deserialize(res);

      this.setReducedState({
        data,
        loading: false,
        error: null,
      });
    } catch (error) {
      this.setReducedState({
        loading: false,
        error,
      });
    }
  };

  actions = {
    fetch: this.request,
  };

  componentDidMount() {
    if (!this.props.disableInitialFetch) {
      this.request();
    }
  }

  render() {
    const { children } = this.props;

    const value = {
      state: this.state,
      actions: this.actions,
    };

    return (
      <QueryContext.Provider value={value}>
        {typeof children === 'function' ? children(value) : children}
      </QueryContext.Provider>
    );
  }
}

Customize its behavior

The Query component is basically a small component which provides the capabilities of fetch as a component. Drop it somewhere in your React tree where you need data from a server and your code stays readable.

Server communication does not consist just of the consuming GET request. Often you want to trigger a request on user interaction to create, update or delete an entity.

We can alter the behavior of the Query component on user interaction. Basically we can alter every fetch option to be able use different HTTP methods like POST or DELETE or change the URL.

Takeaway

Integrating server communication into your components can clutter your code. Extract recurring request logic to a composite component to be able to reuse it in your application.This approach helps you to keep your code DRY and leverages the component based approach of React and the separation of concerns.

Try it on CodeSandbox

CodeSandBox | Creating a reusable React Query component for your REST API

👋 Hi! I am Tim Kolberger. I work at Incloud in Darmstadt, Germany, as a full stack web developer. I ❤️ React, GraphQL and JavaScript.

© 2021 Tim KolbergerDatenschutzerklärung