gene b.
gene b.

Reputation: 12026

Populate a state variable after an async fetch and the setting of another state variable

In my component initialization I need to do the following steps, in this order:

  1. Fetch a server-side data request.
  2. Set a state var. with the result.
  3. Populate another state var. based on the state var. obtained in (2)

If I do the following

const [myData, setMyData] = useState(null);
const [additionalObject, setAdditionalObject] = useState([]);

useEffect(async () => {    
    const response = await axios.get(url); // Step 1
    setMyData(response.data); // Step 2
    populateAdditionalObject(); // Step 3   
}, []); 

// --- Custom Method: ADDITIONAL STATE VAR DEPENDENT ON "myData" ---
const populteAdditionalObject = () => {
    if (myData != null) {
        //... set "additionalObject" as appropriate based on "myData" ...
    }
}

I see that the setter in Step 2 is not immediate, and when I come to populateAdditionalObject which checks myData, it's not populated yet.

To solve this problem, I added a new useEffect pointing to the variable myData to track when it changes. I did the following:

// On initialization, fetch UserInfo
useEffect(async () => {    
    const response = await axios.get(url); // Step 1
    setMyData(response.data); // Step 2
}, []); 
// On update of myData, populate "additionalObject" state var
useEffect(() => {
    populateAdditionalObject(); 
}, [myData]);

This does work, but now I get a warning in the browser, which I didn't have before:

 Line 26:5:   React Hook useEffect has a missing dependency: 'populateAdditionalObject'. Either include it or remove the dependency array  react-hooks/exhaustive-deps

So how do I solve this with the right sequence and no browser warnings?

Upvotes: 0

Views: 143

Answers (2)

David
David

Reputation: 219016

Add the function to the dependency array as the warning suggests:

useEffect(() => {
  populateAdditionalObject(); 
}, [myData, populateAdditionalObject]);

Then to ensure that the function itself doesn't change unless it needs to, wrap it in a useCallback with its own dependency array:

const populteAdditionalObject = useCallback(() => {
  if (myData != null) {
    //... set "additionalObject" as appropriate based on "myData" ...
  }
}, [myData]);

Though the useEffect dependency array may also start to complain that it has an unnecessary dependency, myData. (Because myData isn't actually used in the effect, at least not directly.) But since changes to myData should result in a new instance of the callback, it should work with only that in the dependency array:

useEffect(() => {
  populateAdditionalObject(); 
}, [populateAdditionalObject]);

Alternatively, if you don't want to chain the dependencies this much, you can tell the code linter to ignore that dependency array:

useEffect(() => {
  populateAdditionalObject();
  // eslint-disable-next-line
}, [myData]);

This option should be used sparingly, it's basically your way of saying "I know something about this that the linter/transpiler/etc. doesn't know, trust me." Which in this case is true, because what you know is that this effect truly should only be run when myData changes, and that populateAdditionalObject is always going to be re-defined on a component render and will always have the correct closures, etc.

That last part is important, and why the dependency for useCallback included myData. Because you don't want to call a stale populateAdditionalObject with a stale reference to myData because you'd end up with the same original problem you had, but more difficult to diagnose.

It's generally best to follow the linter warnings and maintain the dependency arrays completely. While this option is fine on rare occasions, more often than not I think that suppressing the warnings is covering up what should be a design change.


As a possible design change and another alternative, you could skip the second useEffect entirely and pass the necessary data to the function:

useEffect(async () => {    
  const response = await axios.get(url); // Step 1
  setMyData(response.data); // Step 2
  populateAdditionalObject(response.data); // Step 3   
  // eslint-disable-next-line
}, []);

const populteAdditionalObject = (data) => {
  if (data != null) {
    //... set "additionalObject" as appropriate based on "data" ...
  }
}

This is probably the simplest approach overall and would be preferred if the function doesn't have some other reason not to accept the data as an argument. (Something we don't see in the code shown.)

Upvotes: 1

Suraj Yakkha
Suraj Yakkha

Reputation: 64

Pass populateAdditionalObject function in the useEffect hook

useEffect(() => {
populateAdditionalObject(); 
}, [populateAdditionalObject]);

Upvotes: 0

Related Questions