Search code examples
javascriptreactjsuse-effect

Have a custom hook run an API call only once in useEffect


I have a custom hook in my React application which uses a GET request to fetch some data from the MongoDB Database. In one of my components, I'm reusing the hook twice, each using different functions that make asynchronous API calls.

While I was looking at the database logs, I realized each of my GET requests were being called twice instead of once. As in, each of my hooks were called twice, making the number of API calls to be four instead of two. I'm not sure why that happens; I'm guessing the async calls result in re-renders that aren't concurrent, or there's somewhere in my component which is causing the re-render; not sure.

Here's what shows up on my MongoDB logs when I load a component:

MongoDB log

I've tried passing an empty array to limit the amount of time it runs, however that prevents fetching on reload. Is there a way to adjust the custom hook to have the API call run only once for each hook?

Here is the custom hook which I'm using:

const useFetchMongoField = (user, id, fetchFunction) => {
  const [hasFetched, setHasFetched] = useState(false);
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      if (!user) return;
      try {
        let result = await fetchFunction(user.email, id);
        setData(result);
        setHasFetched(true);
      } catch (error) {
        setError(error.message);
      }
    };

    if (data === null) {
      fetchData();
    }
  }, [user, id, fetchFunction, data]);

  return { data, hasFetched, error };
};

This is one of the components where I'm re-using the custom hook twice. In this example, getPercentageRead and getNotes are the functions that are being called twice on MongoDB (two getPercentageRead calls and two getNotes calls), even though I tend to use each of them once.

const Book = ({ location }) => {
  const { user } = useAuth0();
  const isbn = queryString.parse(location.search).id;

  const { data: book, hasFetched: fetchedBook } = useFetchGoogleBook(isbn);
  const { data: read, hasFetched: fetchedPercentageRead } = useFetchMongoField(
    user,
    isbn,
    getPercentageRead
  );
  const { data: notes, hasFetched: fetchedNotes } = useFetchMongoField(
    user,
    isbn,
    getNotes
  );

  if (isbn === null) {
    return <RedirectHome />;
  }
  return (
    <Layout>
      <Header header="Book" subheader="In your library" />

      {fetchedBook && fetchedPercentageRead && (         
        <BookContainer
          cover={book.cover}
          title={book.title}
          author={book.author}
          date={book.date}
          desc={book.desc}
          category={book.category}
          length={book.length}
          avgRating={book.avgRating}
          ratings={book.ratings}
          language={book.language}
          isbn={book.isbn}
          username={user.email}
          deleteButton={true}
          redirectAfterDelete={"/"}
        >
          <ReadingProgress
            percentage={read}
            isbn={book.isbn}
            user={user.email}
          />          
        </BookContainer>        
      )}

      {!fetchedBook && (
        <Wrapper minHeight="50vh">
          <Loading
            minHeight="30vh"
            src={LoadingIcon}
            alt="Loading icon"
            className="rotating"
          />
        </Wrapper>
      )}
      <Header header="Notes" subheader="All your notes on this book">
        <AddNoteButton
          to="/add-note"
          state={{
            isbn: isbn,
            user: user,
          }}
        >
          <AddIcon color="#6b6b6b" />
          Add Note
        </AddNoteButton>
      </Header>
      {fetchedNotes && (
        <NoteContainer>
          {notes.map((note) => {
            return (
              <NoteBlock
                title={note.noteTitle}
                date={note.date}
                key={note._noteID}
                noteID={note._noteID}
                bookID={isbn}
              />
            );
          })}
          {notes.length === 0 && (
            <NoNotesMessage>
              You don't have any notes for this book yet.
            </NoNotesMessage>
          )}
        </NoteContainer>
      )}
    </Layout>
  );
};

Solution

  • I was able to fix this issue.

    The problem was I was assuming the user object was remaining the same across renders, but some of its properties did in fact change. I was only interested in checking the email property of this object which doesn't change, so I only passed user?.email to the dependency array which solved the problem.