skip to Main Content

There is an input field that will update a state called q to whatever the user enters, and the code inside useEffect will do a search with q.

If the length of q is <= 2, then I need to empty the stuff state.

I am having an issue where the changes from setStuff(thing) from the previous useEffect render, is overwriting setStuff([]) happening in the current useEffect render

useEffect(()=>{
   if (q.length > 2) {
      fetchSomething(q).then(thing => setStuff(thing))
   } else {
      setStuff([]);
   }
}, [q]);

3

Answers


  1. If you don’t have a a system setup where you can easily cancel requests you can just ignore the response you are getting back when you no longer need it.

    This is untested, but this should work just fine.

    useEffect(()=>{
       // Create a variable to signify whether the useEffect
       // has run its clean up function.
       let isDisposed = false;
    
       if (q.length > 2) {  
          fetchSomething(q).then(thing => {
             // check if the useEffect clean up function has
             // run, we don't want to update state if this
             // is not the most recent (current) use effect
             if (!isDisposed) {
                setStuff(thing);
             }
          })
       }
       else {
          setStuff([]);
       }
    
       // Specify the clean up function.
       return () => {
          // Set the variable to signify the clean up
          // function has been run.
          isDisposed = true;
       }
    }, [q]);
    
    Login or Signup to reply.
  2. I think the problem is that you are not canceling the previous fetch request when the q changes, so you might end up with a race condition where the old request finishes after the new one and overwrites the state. You can try to use an abort controller to cancel the previous request when the q value changes, like this:

    const abortRef = useRef(null);
    
    useEffect(() => {
      if (abortRef.current) {
        abortRef.current.abort();
      }
    
      if (q.length <= 2) {
        setStuff([]);
      } else {
        const controller = new AbortController();
        abortRef.current = controller;
    
        fetchSomething(q, controller.signal)
          .then(setStuff)
          .catch((error) => {
            console.log(`${error.name}: ${error.message}`);
          });
      }
    }, [q]);
    

    You can see the whole example here.

    If you are using axios, you can set the request like this:

    axios
      .get(`/api/something?q=${q}`, {
        cancelToken: controller.signal, // Pass the signal as the cancelToken
      })
      .then((response) => {
        setStuff(response.data);
      })
      .catch((error) => {
        if (error.name === "AbortError") {
          return;
        }
        console.error(error);
      });
    
    Login or Signup to reply.
  3. the recommended way is to use a cleanup callback to clean states and listeners when the component unmount (for a re-rendring in you case)

    import React, { useState, useEffect, useRef } from "react";
    
    const fetchSomething = (q, signal) => {
      return new Promise((resolve, reject) => {
        if (signal.aborted) {
          reject(new DOMException("Aborted", "AbortError"));
          return;
        }
    
        const delay = Math.floor(Math.random() * 1000) + 500;
    
        const timeoutId = setTimeout(() => {
          const data = Array.from({ length: q.length }, () =>
            Math.floor(Math.random() * 100)
          );
    
          resolve(data);
        }, delay);
    
        signal.addEventListener("abort", () => {
          clearTimeout(timeoutId);
          reject(new DOMException("Aborted", "AbortError"));
        });
      });
    };
    
    const Example = () => {
      const [q, setQ] = useState("");
    
      const [stuff, setStuff] = useState([]);
    
      const controllerRef = useRef(null);
    
      const refQ = useRef(null);
    
      useEffect(() => {
        const element = refQ.current;
    
        if (q.length > 2) {
          const controller = new AbortController();
          controllerRef.current = controller;
    
          fetchSomething(q, controller.signal)
            .then((thing) => {
              setStuff(thing);
            })
            .catch((error) => {
              if (error.name === "AbortError") {
                return;
              }
              console.error(error);
            });
        }
        return () => {
          if (controllerRef.current) {
            controllerRef.current.abort();
          }
    
          if (element.value.length <= 2) {
            setStuff([]);
          }
        };
      }, [q]);
    
      return (
        <div>
          <h1>Simplest example</h1>
          <input
            ref={refQ}
            type="text"
            value={q}
            onChange={(e) => setQ(e.target.value)}
            placeholder="Enter something..."
          />
          <p>Stuff: {JSON.stringify(stuff)}</p>
        </div>
      );
    };
    
    export default Example;
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search