API request with React Hooks, HOC or Render Prop


    Consider the implementation of requesting data to the API using the new friend React Hooks and the good old friends Render Prop and HOC (Higher Order Component). Find out if a new friend is really better than the old two.


    Life does not stand still, React is changing for the better. In February 2019, React Hooks appeared in React 16.8.0. Now in the functional components you can work with the local state and perform side effects. Nobody believed that it was possible, but everyone always wanted it. If you are not up to date with details, click here for details .


    React Hooks make it possible to finally abandon patterns such as HOC and Render Prop. Because during use, a number of claims have accumulated against them:


    RPropHoc
    1. Many wrapper components that are difficult to understand in React DevTools and in code.(◕︵◕)(◕︵◕)
    2. It is difficult to type (Flow, TypeScript).(◕︵◕)
    3. It is not obvious from which HOC which props the component receives, which complicates the debugging process and understanding how the component works.(◕︵◕)
    4. Render Prop most often does not add layout, although it is used inside JSX.(◕︵◕)
    5. Key collision props. When transmitting props from parents, the same keys can be overwritten with values ​​from the HOC.(◕︵◕)
    6. It is difficult to read git diff, since all indentation in JSX is shifted when wrapping JSX in Render Prop.(◕︵◕)
    7. If there are several HOC, then you can make a mistake with the sequence of the composition. The correct order is not always obvious, since the logic is hidden inside the HOC. For example, when we first check whether the user is authorized, and only then we request personal data.(◕︵◕)

    In order not to be unfounded, let's look at an example of how React Hooks is better (or maybe worse) Render Prop. We will consider Render Prop, not HOC, since in implementation they are very similar and HOC has more drawbacks. Let's try to write a utility that processes the data request to the API. I am sure that many have written this in their lives hundreds of times, well, let’s see if it can be better and easier.


    For this we will use the popular axios library. In the simplest scenario, you need to process the following states:


    • data acquisition process (isFetching)
    • data successfully received (responseData)
    • error receiving data (error)
    • cancellation of the request, if during its execution the request parameters have changed, and you need to send a new
    • canceling a request if this component is no longer in the DOM

    1. Simple scenario


    We will write the default state and a function (reducer) that changes state depending on the result of the request: success / error.


    What is Reducer?

    For reference. Reducer came to us from functional programming, and for most JS developers from Redux. This is a function that takes a previous state and action and returns the next state.


    const defaultState = {
      responseData: null,
      isFetching: true,
      error: null
    };
    function reducer1(state, action) {
      switch (action.type) {
        case "fetched":
          return {
            ...state,
            isFetching: false,
            responseData: action.payload
          };
        case "error":
          return {
            ...state,
            isFetching: false,
            error: action.payload
          };
        default:
          return state;
      }
    }

    We reuse this function in two approaches.


    Render prop


    class RenderProp1 extends React.Component {
      state = defaultState;
      axiosSource = null;
      tryToCancel() {
        if (this.axiosSource) {
          this.axiosSource.cancel();
        }
      }
      dispatch(action) {
        this.setState(prevState => reducer(prevState, action));
      }
      fetch = () => {
        this.tryToCancel();
        this.axiosSource = axios.CancelToken.source();
        axios
          .get(this.props.url, {
            cancelToken: this.axiosSource.token
          })
          .then(response => {
            this.dispatch({ type: "fetched", payload: response.data });
          })
          .catch(error => {
            this.dispatch({ type: "error", payload: error });
          });
      };
      componentDidMount() {
        this.fetch();
      }
      componentDidUpdate(prevProps) {
        if (prevProps.url !== this.props.url) {
          this.fetch();
        }
      }
      componentWillUnmount() {
        this.tryToCancel();
      }
      render() {
        return this.props.children(this.state);
      }
    

    React hooks


    const useRequest1 = url => {
      const [state, dispatch] = React.useReducer(reducer, defaultState);
      React.useEffect(() => {
        const source = axios.CancelToken.source();
        axios
          .get(url, {
            cancelToken: source.token
          })
          .then(response => {
            dispatch({ type: "fetched", payload: response.data });
          })
          .catch(error => {
            dispatch({ type: "error", payload: error });
          });
        return source.cancel;
      }, [url]);
      return [state];
    };

    By url from the used component we get the data - axios.get (). We process success and error, changing state through dispatch (action). Return state to the component. And do not forget to cancel the request if the url changes or if the component is removed from the DOM. It's simple, but you can write in different ways. We highlight the pros and cons of the two approaches:


    HooksRProp
    1. Less code.(◑‿◐)
    2. Calling the side effect (requesting data in the API) is easier to read, as it is linearly written, not spread over the component life cycles.(◑‿◐)
    3. The request cancellation is written immediately after the request is called. All in one place.(◑‿◐)
    4. Simple code that describes tracking parameters for triggering side effects.(◑‿◐)
    5. Obviously, in what component life cycle our code will be executed.(◑‿◐)

    React Hooks allow you to write less code, and this is an indisputable fact. This means that the effectiveness of you as a developer is growing. But you have to master a new paradigm.


    When there are names of component life cycles, everything is very clear. First, we get the data after the component appeared on the screen (componentDidMount), then we get it again if props.url has changed and before that we don’t forget to cancel the previous request (componentDidUpdate), if the component has been removed from the DOM, then cancel the request (componentWillUnmount) .


    But now we cause a side effect directly in the render, we were taught that this is impossible. Although stop, not really in the render. And inside the useEffect function, which will perform asynchronously something after each render, or rather commit and render the new DOM.


    But we don’t need after each render, but only on the first render and in case of changing the url, which we indicate as the second argument to useEffect.


    New paradigm

    Understanding how React Hooks work requires awareness of new things. For example, the difference between the phases: commit and render. In the render phase, React calculates which changes to apply in the DOM by comparing with the result of the previous render. And in the commit phase, React applies these changes to the DOM. It is in the commit phase that the methods are called: componentDidMount and componentDidUpdate. But what is written in useEffect will be called asynchronously after the commit and, therefore, will not block the DOM rendering if you suddenly accidentally decide to count a lot in the side effect.


    Conclusion - use useEffect. Writing less and safer.


    And one more great feature: useEffect can clean up after the previous effect and after removing the component from the DOM. Thanks to Rx who inspired the React team for this approach.


    Using our utility with React Hooks is also much more convenient.


    const AvatarRenderProp1 = ({ username }) => (
      
        {state => {
          if (state.isFetching) {
            return "Loading";
          }
          if (state.error) {
            return "Error";
          }
          return avatar;
        }}
      
    );

    const AvatarWithHook1 = ({ username }) => {
      const [state] = useRequest(`https://api.github.com/users/${username}`);
      if (state.isFetching) {
        return "Loading";
      }
      if (state.error) {
        return "Error";
      }
      return avatar;
    };

    The React Hooks option again looks more compact and obvious.


    Cons Render Prop:


    1) it is not clear whether the layout is added or only logic
    2) if you need to process the state from Render Prop in the local state or in the life cycles of the child component, you will have to create a new component


    Add a new functionality - receiving data with new parameters by user action. I wanted, for example, a button that gets an avatar of your favorite developer.


    2) Updating user action data


    Add a button that sends a request with a new username. The simplest solution is to store the username in the local state of the component and transfer the new username from state, not props as it is now. But then we will have copy-paste wherever similar functionality is needed. So we will take out this functionality to our utility.


    We will use it like this:


    const Avatar2 = ({ username }) => {
     ...
         
     ...
    };

    Let's write an implementation. Below are written only the changes compared to the original version.


    function reducer2(state, action) {
      switch (action.type) {
       ...
       case "update url":
          return {
            ...state,
            isFetching: true,
            url: action.payload,
            defaultUrl: action.payload
          };
        case "update url manually":
          return {
            ...state,
            isFetching: true,
            url: action.payload,
            defaultUrl: state.defaultUrl
          };
       ...
      }
    }

    Render prop


    class RenderProp2 extends React.Component {
      state = {
        responseData: null,
        url: this.props.url,
        defaultUrl: this.props.url,
        isFetching: true,
        error: null
      };
      static getDerivedStateFromProps(props, state) {
        if (state.defaultUrl !== props.url) {
          return reducer(state, { type: "update url", payload: props.url });
        }
        return null;
      }
     ...
     componentDidUpdate(prevProps, prevState) {
       if (prevState.url !== this.state.url) {
         this.fetch();
       }
     }
     ...
     update = url => {
       this.dispatch({ type: "update url manually", payload: url });
     };
     render() {
       return this.props.children(this.state, this.update);
     }
    }

    React hooks


    const useRequest2 = url => {
     const [state, dispatch] = React.useReducer(reducer, {
        url,
        defaultUrl: url,
        responseData: null,
        isFetching: true,
        error: null
      });
     if (url !== state.defaultUrl) {
        dispatch({ type: "update url", payload: url });
      }
     React.useEffect(() => {
       …(fetch data);
     }, [state.url]);
     const update = React.useCallback(
       url => {
         dispatch({ type: "update url manually", payload: url });
       },
       [dispatch]
     );
     return [state, update];
    };

    If you carefully looked at the code, you noticed:


    • url began to be stored inside our utility;
    • defaultUrl appeared to identify that the url was updated via props. We need to monitor the change of props.url, otherwise a new request will not be sent;
    • added the update function, which we return to the component to send a new request by clicking on the button.

    Обратите внимание с Render Prop нам пришлось воспользоваться getDerivedStateFromProps для обновления локального state в случае изменения props.url. А с React Hooks никаких новых абстракций, можно сразу в рендере вызывать обновление state — ура, товарищи, наконец!


    Единственно усложнение с React Hooks — пришлось мемоизировать функцию update, чтобы она не изменялась между обновлениями компонента. Когда как в Render Prop функция update является методом класса.


    3) Опрос API через одинаковый промежуток времени или Polling


    Давайте добавим еще один популярный функционал. Иногда нужно постоянно опрашивать API. Мало ли ваш любимый разработчик поменял аватарку, а вы не в курсе. Добавляем параметр интервал.


    Использование:


    const AvatarRenderProp3 = ({ username }) => (
     
    ...

    const AvatarWithHook3 = ({ username }) => {
     const [state, update] = useRequest(
       `https://api.github.com/users/${username}`, 1000
     );
    ...

    Реализация:


    function reducer3(state, action) {
     switch (action.type) {
       ...
       case "poll":
         return {
           ...state,
           requestId: state.requestId + 1,
           isFetching: true
         };
       ...
     }
    }

    Render Prop


    class RenderProp3 extends React.Component {
     state = {
      ...
      requestId: 1,
     }
     ...
     timeoutId = null;
     ...
     tryToClearTimeout() {
       if (this.timeoutId) {
         clearTimeout(this.timeoutId);
       }
     }
     poll = () => {
       this.tryToClearTimeout();
       this.timeoutId = setTimeout(() => {
         this.dispatch({ type: 'poll' });
       }, this.props.pollInterval);
     };
     ...
     componentDidUpdate(prevProps, prevState) {
       ...
       if (this.props.pollInterval) {
         if (
           prevState.isFetching !== this.state.isFetching &&
           !this.state.isFetching
         ) {
           this.poll();
         }
         if (prevState.requestId !== this.state.requestId) {
           this.fetch();
         }
       }
     }
     componentWillUnmount() {
       ...
       this.tryToClearTimeout();
     }
     ...
    

    React Hooks


    const useRequest3 = (url, pollInterval) => {
      const [state, dispatch] = React.useReducer(reducer, {
        ...
        requestId: 1,
      });
     React.useEffect(() => {
       …(fetch data)
     }, [state.url, state.requestId]);
     React.useEffect(() => {
       if (!pollInterval || state.isFetching) return;
       const timeoutId = setTimeout(() => {
         dispatch({ type: "poll" });
       }, pollInterval);
       return () => {
         clearTimeout(timeoutId);
       };
     }, [pollInterval, state.isFetching]);
    ...
    }

    Появился новый prop — pollInterval. При завершении предыдущего запроса через setTimeout мы инкрементируем requestId. С хуками у нас появился еще один useEffect, в котором мы вызываем setTimeout. А старый наш useEffect, который отправляет запрос стал следить еще за одной переменной — requestId, которая говорит нам, что setTimeout отработал, и пора уже запрос отправлять за новой аватаркой.


    В Render Prop пришлось написать:


    1. сравнение предыдущего и нового значения requestId и isFetching
    2. очистить timeoutId в двух местах
    3. добавить классу свойство timeoutId

    React Hooks позволяют писать коротко и понятно то, что мы привыкли описывать подробнее и не всегда понятно.


    4) What next?
    We can continue to expand the functionality of our utility: accepting different configurations of query parameters, caching data, converting a response and errors, forcibly updating data with the same parameters — routine operations in any large web application. On our project, we have long ago taken this out into a separate (attention!) Component. Yes, because it was a Render Prop. But with the release of Hooks, we rewrote the function (useAxiosRequest) and even found some bugs in the old implementation. You can see and try here .


    Also popular now: