posts  

How to fetch with useEffect Jul.4/2023

0. Whydunnit?!

Sharing, Reminder and Optimization. If you want to Jump to the subject without care about the reason just go to chapter III & IV. I and II is for those who care about why it should & how its.

Whydunnit

I. Is that what we really need?

Okay, I assume you and many of us know about how the useEffect lifecycle works or at least have an understanding of it. It's common to use useEffect for fetching data, but we should consider if it's really necessary. Fetching data with useEffect can have downsides. For instance, behind the scenes, useEffect may render many things, and even though we can optimize it with useCallback or memo, we still need to handle those. Assuming useEffect is the perfect choice for you, then the next question is 'are you fetching the data correctly?' Keep in mind that useEffect is triggered automatically based on dependencies.

II. So?, Whats bad?!

Like I mentioned before, since useEffect triggers automatically, it must be used with caution to avoid triggering uncontrollable fetches and introducing performance issue or potential new bugs. It needs to be specific and well-defined. For example, populating initial data on the first mount so useEffect doesn't need to have dependencies, allowing us to control the behavior. This approach is commonly used in client-side rendering. To fetch new data, we can add dependencies such as global variables like isLogin, user, or subs is still valid way, considering that the variable rarely changes. However, it's important to avoid adding excessive dependencies to useEffect without robust validation, especially in the case of fetching data.

Fetching with useEffect seems like perfect solution sometimes, 'if.. we dont really read the doc'. If you knew the behavior of react strict mode, now you can see the drawbacks. react strict mode would enable render twice for first init for debuggin reason, Yup 2x render means 2x fetching, even tho its have no dependencies on it.

Here's how we can tackling the problems:

const ref = useRef(false); 
/** 
* 0r you can use useState 
* But its not recomended since useState effect the state and ref manipulate the dom 
**/
useEffect(() => {
  if (!ref.current) return () => {ref.current = true} // First Init
  console.info('fetching here');
},[]);

See its already bothersome, Oh boy, its not over yet, lets take a look from useEffect lifecycle,useEffect lifecylenow we notice that API call will be made after complete render, waiting API to complete and then update the UI so its not first init with data for CSR. This thing will cause CLS which is bad for SEO.

III. Then what we should do?

Well they are many variant options, First one (which always works): Eliminate the problems by not using useEffect ¯\_(ツ)_/¯, Well, that still counts as an option, right..? Second option: Humans are smart; they already crafted something like useQuery where API being called in Show Loading UI until it finish and then render the UI with data, so its looks like first init with data, actually you could create your own custom useHooks for this, that will return the isLoading, err, data and etc, you got the idea. Sadly useQuery is not officially documented or integrated by React itself. Third option: If you can move to SSR and it meets your needs, or just bring the first initialized page with populated data, then just do it, because it will improve your SEO, performance and reduce potential bugs, dont take big risk for a simple thing when you can do it vice versa. Other options include: 4) SWR, 5) URQL, 6) react-router loaders, 7) Apollo Client useQuery, 8) TanStack useQuery.

IV. Dead End?!

Alright to finish this topic I must assume at least now we know when to really use useEffect for fetching. If we can avoid it, just do it. Maybe simplify the data flow, simplify the logic or just integrate with a button, etc. Now, if you really need and must use it, at least do it right.

Okay one thing I want to mention; In above I was talk about useEffect can be trigger and render many times, we all know that but just bear with me keep that in mind for now I mark that as [0].

Now the case zero: useEffect for first populate.

// NOT USING "use strict"; or react-strict-mode
const [posts, setPosts] = useState<post>([])

const getPosts = async () => {
  try {
    await fetch('https://jsonplaceholder.typicode.com/posts')
      .then(res => res.json())
      .then(res => setPosts(res));
  } catch(e:any) {
    console.error(e.message)
  }
}

useEffect(()=> getPosts() ,[])

/**
* So we will use this source base to follow along;
* This is what commonly poeple be thought, at least from people around me. And this is not good;
* So this `useEffect` will be trigger once; and the data being fetch thats it; 
**/

Case one: When the data need to refetch after some condition, but still collect the same data to overwrite the old data, just to check if there is any changes with the data.

// NOT USING "use strict"; or react-strict-mode
const [ posts, setPosts ] = useState([])
const { state : { delSignal } } = useContext(SignalContext)

const getPosts = async () => {
  try {
    await fetch('https://jsonplaceholder.typicode.com/posts')
      .then(res => res.json())
      .then(res => setPosts(res));
  } catch(e:any) {
    console.error(e.message)
  }
}

useEffect(()=> getPosts() ,[delSignal])

/**
* So `useEffect` will hit in first mount 
* and whenever `delSignal` comes along; 
**/

Case two: When useEffect will be use with pagination, trigger refetch and adding new data every time offsite.

// NOT USING "use strict"; or react-strict-mode
const [ posts, setPosts ] = useState([])
const ref = useRef(null) // ref will be put in bottom element, ('imagine a scroll social media app')
const isInView = useInView(ref)

const getNextFivePosts = async () => {
  try {
    const lastPostId = posts.slice(-1)?.id || ""
    await fetch('https://jsonplaceholder.typicode.com/pagination/posts/'+ lastPostId) // This is just example endpoint, using offsite pagination
      .then(res => res.json())
      .then(res => setPosts(prev=> ([...prev, ...res])));
  } catch(e:any) {
    console.error(e.message)
  }
}

useEffect(()=> getNextFivePosts() ,[isInView])

/**
* `useEffect` will hit in first mount, 
* and whenever element `ref` in `isInView` offsite; 
* when its hit the request will ask more 5 posts, by last id post they have, 
* so on and so on.
**/

Now Believe me guys this three ugly example is just for explain this one simple thing. Remember [0], this is what people miss alot, when we dealing with async and useEffect and send request call to API, those thing will be still running, until they get what they request. Now the problems will happen when user restart or refresh their browser over and over, async and the request will still running, pile up and bite the computing resource for thing that will not be use.

In case zero thing still can get messy even got trigger one, like I said the request can get stacked in background, and it will bite the computing resource. In the case one, when the `del` request flood, the same thing will happend, chewing the computing resource-san again. In the case three, more worse, it can bite and cheing the computing resource in the background but also can introducing new duplicated bug, we want first init 5 and next would be 10, it can first 10 and next would be 20. So how?, we can handle fetch thing in useEffect.

simple! we need to abort old request before next request is passed. In JavaScript or in web request we have thing called AbortControllerWhere we will put in header request. This allow us to combine useEffect lifecyle to control the fetch behavior so its doesnt waste resource, for thing that we dont want; It also help us to reduce duplicated bugs or exceeded request limit from potential unwated request by controlling request with lifecyle.

// NOT USING "use strict"; or react-strict-mode
const [ posts, setPosts ] = useState([])

/* *
* First we need to refactor all that crap;
* by moving all into `useEffect` with anon func;
* */

/* *
* useEffect(async()=>{
*   dont use it like this
*   async on cb useEffect is bad due to lifecyle
* },[])
* */

useEffect(()=> {
  const controller = new AbortController(); //  Init the controller;
  const signal = controller.signal; 
  /**
  * Don't forget the semicolon, or it will throw err, 
  * due to anon func think its running `signal` if we dont end it with semicolon; 
  * 'controller.signal() X', 'controller.signal; ()()' good one; 
  **/

  // Creating Anon func and running it
  (async()=> {
    try {
      await fetch('https://jsonplaceholder.typicode.com/posts',
        { signal } 
/** 
* This place(object where `signal` is) is called `options` where 
* we usually put something in
* like header, method, mode, credentials, etc 
* but we tend dont know about signal or forget about how important signal for optimizing.
**/
      ).then(res => res.json())
        .then(res => setPosts(res));
    } catch(e: any) {
     console.error(e.message)
    }
  })();

  /**
  * Unmount, lifecyle ended or abort 
  * whatever request happend inside this useEffect;
  * We can also attch on button, but we need to 
  * init controller outside, so it can be access able, and 
  * dont forget to throw the signal to the fetch `option`;
  **/
  return ()=> controller.Abort();
} ,[])

Clean version without comment, it also can be implement like 3 version of example, first init to populate, refetching when data changes, refetching with pagination.

const [ posts, setPosts ] = useState([]);

useEffect(() => {
  const controller = new AbortController(); 
  const signal = controller.signal;

  (async () => {
    try {
      await fetch('https://jsonplaceholder.typicode.com/posts', { signal })
        .then(async res => setPosts(await res.json()))
    } catch(e: any) {
     console.error(e.message)
    }
  })();

  return () => controller.Abort();
},[])

Disclaimer

  • Please take a look at the date when this post was made, as some versions may vary depending on the topic or be deprecated.
  • Im A wrong?; Bwahahah thx for noticing, please hit me at @indratc18, @expitc, or whatever you can reach me, Im open to discuss and much thx for sharing exp potions & help me ( ๑‾̀◡‾́)σ".
  • Have question? or Have effective way?, Go ahead ლ(▀̿̿Ĺ̯̿̿▀̿ヾ), lets sharing!
  • Typo?, Grammar? Just ignore it as long as you know it. Im a still a human being y'all.. _(┐「ε:)_
© 2023 Indra TC. All Rights Reserved.