Infinite rendering of component when fetching data in useEffect wrapped in HOC

Viewed 275

What I am trying to do:

  1. fetch data in useEffect from my component
  2. update loading state from the HOC
  3. return wrapped component /display data

App.js

import axios from "axios";
import { useEffect, useState } from "react";
import withFetch from "./withFetch";

const App = ({ setLoading }) => {
  const [data, setData] = useState([]);

  useEffect(() => {
    setLoading(true);

    async function fetchData() {
      await axios
        .get("https://jsonplaceholder.typicode.com/todos")
        .then((res) => {
          setData(res.data);
          setLoading(false);
        });
    }

    fetchData();
  }, []);

  return (
    <div>
      {data.map((item) => (
        <p>{item}</p>
      ))}
    </div>
  );
};

export default withFetch(App);

withFetch /HOC:

import React, { useState } from "react";

const withFetch = (WrappedComponent) => {
  const WithFetch = (props) => {
    const [isLoading, setLoading] = useState(false);

    if (isLoading) return <h1>Loading..</h1>;

    return <WrappedComponent setLoading={setLoading} {...props} />;
  };

  return WithFetch;
};

export default withFetch;

What goes wrong:

  • infinite loop from/to HOC and my component

I made a very simple demo in CodeSandbox to demonstrate my problem: https://codesandbox.io/s/reverent-cache-bb044?file=/src/App.js

2 Answers

As soon as App calls setLoading(true), WithFetch no longer renders WrappedComponent, so App is unmounted.

Then eventually the request succeeds, and calls setLoading(false), which renders a new App, which invokes another request.

Instead, I think you want to co-locate the fetching code with the isLoading state.

// withFetch.jsx
// The withFetch HOC knows how to conditionally display a component
// depending on whether data has been fetch, and actually fetches.
// But, it doesn't know _where_ the data came from (because presumably
// you will want to reuse this for different data sets)

const withFetch = (WrappedComponent) => {
  const WithFetch = (props) => {
    const [isLoading, setLoading] = useState(false);
    const [data, setData] = useState([]);

    useEffect(() => {
      setLoading(true);

      async function fetchData() {
        await axios.get(props.url).then((res) => {
          setData(res.data);
          setLoading(false);
        });
      }

      fetchData();
    }, [props.url]);

    if (isLoading) {
      return <h1>Loading..</h1>;
    } else {
      return <WrappedComponent data={data} {...props} />;
    }
  };

  return WithFetch;
};


// Display.jsx
// The Display component knows how to display data after it has
// been fetched, but knows nothing about how to fetch the data
// or where it comes from.

const Display = ({ data }) => {
  return (
    <div>
      {data.map((item) => (
        <p>{item}</p>
      ))}
    </div>
  );
};

export default withFetch(Display);


// App.jsx
// The App component renders the Display component and
// provides the URL where data comes from

const App = () => {
  return <Display url="https://jsonplaceholder.typicode.com/todos" />;
};

// do not wrap App with withFetch

Using a custom hook can eliminate the need for an HOC entirely, if you'd prefer that.

function useFetch(url) {
  const [result, setResult] = useState({
    data: null,
    isLoading: false,
    error: null,
  });

  useEffect(() => {
    async function fetchData() {
      setResult({
        isLoading: true,
        data: null,
        error: null,
      });

      try {
        const res = await axios.get(props.url);
        setResult({
          data: res.data,
          isLoading: false,
          error: null,
        });
      } catch (error) {
        setResult({
          data: null,
          isLoading: false,
          error,
        });
      }
    }

    fetchData();
  }, [url]);

  return result;
}

const Display = ({ data }) => {
  return (
    <div>
      {data.map((item) => (
        <p>{item}</p>
      ))}
    </div>
  );
};

const App = () => {
  const { data, isLoading, error } = useFetch(
    "https://jsonplaceholder.typicode.com/todos"
  );

  if (isLoading) {
    return <h1>Loading..</h1>;
  } else if (error) {
    return <div>Error: {error.message}</div>;
  } else {
    return <Display data={data} />;
  }
};
Related