DEV Community

Cover image for API Data Fetching in React / Next.js

API Data Fetching in React / Next.js

Rashid Shamloo on January 03, 2024

Remember the good old days when you could just use a UseEffect() hook, define an async function with useCallback() and call it inside useEffect(), ...
Collapse
 
lebinhan profile image
Lê Bình An

Though I have used the server side data fetching myself and really like it, I still think that it would be nicer to have code examples for each way of data fetching, then give your in-depth knowledge of each pros and cons, rather than just write down your opinion like this.
I’m looking forward for some extend version of this post with code examples so everyone can discuss together.

Collapse
 
rashidshamloo profile image
Rashid Shamloo

This post is just an overview of different data fetching methods and their pros and cons. Including code would make it unnecessarily long and you can make it as complicated or as simple as you want depending on how in-depth you want to go. from implementing all the functionalities using useEffect() + useState() to implementation details of data fetching libraries that each need a whole new article (eg. for RTK Query: setup, using together with a Redux store, excluding from redux-persist, useQuery, useLazyQuery, useMutation, tags, etc.). you can even introduce a state machine like XState. As for Server Components and Server Actions, they are just async functions that run on the server and you can do whatever you want in them (read/write to a database, fetch data from API, transform data, etc.). again there are implementation details, like configuring caching and revalidation for fetch requests, streaming using Suspense, optimistic updates using useOptimistic(), or even using libraries like next-safe-action.
And I've not even mentioned Next.js API routes since they're just another layer in the middle and depending on whether you want to give access to other apps and the complexity of your application you can opt to use them.

For in-depth implementation details, I suggest referring to each section's links and reading more about them.

Collapse
 
floony7_87 profile image
Fred Lunjevich

I agree. We're not looking for massive detail but even just a sketch of the new and old ways would have been useful (I came here looking for server action info). The title of the article suggests that there is a "how to" coming, that doesn't materialise, thus, the title could be updated to say it's an "overview" for clarity.

Collapse
 
rashidshamloo profile image
Rashid Shamloo

I have now added code examples and more explanations to each section. While it doesn't cover everything, hopefully, it'll provide some information about implementation details and common use cases.

Collapse
 
vbcd profile image
Stanislav Ø.

You don't need to include 'setData' in the dependency array of 'useCallback' - this is peculiar. React guarantees that state setters never change. There's no need for 'useCallback' there at all. You don't need to include 'getData' in the dependency array of 'useEffect' either. This code has a noticeable odor.

Collapse
 
rashidshamloo profile image
Rashid Shamloo

You are correct that state setters don't change and are not needed in the dependency array. I have removed setData from it.

About the useCallback(), other than the fact that not using it will result in the function unnecessarily being recreated on every render, generally, you don't fetch a static 'url' string and instead use a prop or state variable like userId, productId, etc. to fetch the data in which case you will need to use the useCallback hook. You can move the function definition inside the useEffect() and get rid of it but I personally prefer to have my functions separate.

More info: React Docs: Is it safe to omit functions from the list of dependencies?

Also, this is a good example of why you wouldn't want to manually write the data-fetching logic yourself and instead use a library. and why server components are a great addition because they allow you to fetch the data without using any hooks.

Note: I rarely set the dependency array manually and use ESLint to fill it instead (react-hooks/exhaustive-deps). It used to complain about any function/variable used in the hook that was not in the dependency array (I think even the state setters) but now that I checked again it doesn't do it anymore. so it could be the source of some of the confusion.

Note: React Forget will fix some of these problems by remembering/caching all functions by default.

Collapse
 
vbcd profile image
Stanislav Ø.

If you prefer to keep functions separate, you can simply declare them outside the scope of your component. Typically, request handling logic containing fetch or axios should reside in a separate module and be imported. This way, it won't be recreated on every render, and you won't need to use useCallback. However, there is no significant performance gain in avoiding small function recreation unless you are passing it as a prop to another component. Then, the fetching logic that contains state should reside in a separate hook that returns that state, which either success or error data. Including in the dependency array only parameters that clearly could change gives you a clearer mental model of how your code works. Adding instead the entire function to the deps and wrapping it in useCallback is awkward and mentioned as a 'last resort' even in those legacy React docs.

Anyway, the bottom line is - yes, there is little reason not to use a dedicated fetching library nowadays.

Thread Thread
 
rashidshamloo profile image
Rashid Shamloo

Since there's a need to manage the state in the function it can't be declared outside the component (you can move the fetch call to an outside function, but you'd still need to call that in your inner function and manage the state similar to what it is now)

I agree that if someone wants to manually code the fetch logic, it's better to make a custom useFetch() hook and use it instead for easier usage and cleaner code but custom hooks re-run at every re-render and the function will still be recreated on each render (unless useCallback() is used). plus with a custom hook, you'd be passing the url as a prop now (useFetch('url')) and the function has to be added to the dependency of useEffect() and as a result has to be wrapped in useCallback().

example hook:

import { useCallback, useEffect, useState } from 'react';

interface Data {
  name: string;
}

const useFetch = (url: string) => {
  const [data, setData] = useState<Data>();
  const [error, setError] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  const getData = useCallback(
    async (signal: AbortSignal) => {
      setIsLoading(true);
      setIsError(false);
      try {
        const res = await fetch(url, { signal });
        const resJson = await res.json();
        setData(resJson);
      } catch (e) {
        setIsError(true);
        if (typeof e === 'string') setError(e);
        else if (e instanceof Error) setError(e.message);
        else setError('Error');
      } finally {
        setIsLoading(false);
      }
    },
    [url]
  );

  useEffect(() => {
    const controller = new AbortController();
    getData(controller.signal);
    return () => controller.abort();
  }, [getData]);

  return { data, isLoading, isError, error };
};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

usage:

const { data, isLoading, isError, error } = useFetch('url');
Enter fullscreen mode Exit fullscreen mode

While as you said, the overhead for recreating a small function is not much, I prefer not to let that happen. my approach is that if the function is not using the state I define it outside the component (or move it to a util.ts file) and if I define a function inside the component, I always use useCallback() to memorize it.

Thread Thread
 
vbcd profile image
Stanislav Ø. • Edited

"Since there's a need to manage the state in the function it can't be declared outside the component" - It (and everything in the functional world) totally can, you just need to pass your own resolve / reject callbacks. And nothing will be recreated in "useFetch" hook if you declare it inside useEffect. Example of the proper useFetch hook - usehooks-ts.com/react-hook/use-fetch. Notice another important part there - only one state is possible, like "loading", "fetch", or "error" instead of isLoading/isError booleans gymnastics (see more kentcdodds.com/blog/stop-using-isl...)

Thread Thread
 
rashidshamloo profile image
Rashid Shamloo

It's technically possible (by passing all state setters (or dispatch if using useReducer()) to another function and controlling the component's state externally) but it will make reading the code harder than using a useCallback() hook and would only make sense if you'll be using that function in multiple different hooks/components.

StackOverflow: Pass a state setter as an argument to an async function

If you were to do that in the current hook (don't do it :)):

import { Dispatch, SetStateAction, useEffect, useState } from 'react';

interface Data {
  name: string;
}

const getData = async (
  url: string,
  signal: AbortSignal,
  setIsLoading: Dispatch<SetStateAction<boolean>>,
  setIsError: Dispatch<SetStateAction<boolean>>,
  setError: Dispatch<SetStateAction<string>>,
  setData: Dispatch<React.SetStateAction<Data | undefined>>
) => {
  setIsLoading(true);
  setIsError(false);
  try {
    const res = await fetch(url, { signal });
    const resJson = await res.json();
    setData(resJson);
  } catch (e) {
    setIsError(true);
    if (typeof e === 'string') setError(e);
    else if (e instanceof Error) setError(e.message);
    else setError('Error');
  } finally {
    setIsLoading(false);
  }
};

const useFetch = (url: string) => {
  const [data, setData] = useState<Data>();
  const [error, setError] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const controller = new AbortController();
    getData(
      url,
      controller.signal,
      setIsLoading,
      setIsError,
      setError,
      setData
    );
    return () => controller.abort();
  }, [url]);

  return { data, isLoading, isError, error };
};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

I think I have mentioned multiple times by now that declaring the function inside the useEffect() will allow you to drop the useCallback() and you would only need it if you define your functions outside the useEffect().

I've checked that useFetch() hook before, and it is indeed better. but I'm not trying to write a "proper" useFetch() hook and there are most likely more problems with things like error handling in my code. I'm just trying to demonstrate what fetching in a React component would look like without using a library like RTK Query, Server Components, or Server Actions. (I'm using the same state variable names as the ones returned by RTK Query's useQuery() hook)

As the useFetch() hook you linked shows, when managing multiple state variables, using the useReducer() hook is a better solution, and then as the article you linked mentions (and as I mentioned in one of my comments around a month ago) you will most likely want to use a state machine like XState instead.

Thread Thread
 
vbcd profile image
Stanislav Ø.

"If you were to do that in the current hook (don't do it :))" - sure, don't do it this way and this is not at all what I meant, it could be abstracted away with only 2 callbacks.

Thread Thread
 
rashidshamloo profile image
Rashid Shamloo

It may very well be my skill issue but I don't see how that would work. As callbacks, promises, and async/await work the same (throwing in an async function is reject and returning a value is resolve), and you still need to manage the state somewhere in your code. (unless you mean using the then().catch() syntax instead of async/await or something)

Collapse
 
leomunizq profile image
Leonardo Muniz

good article man, so, if i have a page with few components and inside each component, i do 3 fetchs (yeah, its real), which approach is more performant ?