Reputation: 33685
The following code works perfectly :
import { useState, useEffect } from 'react';
const Main = () => {
const [ form, setForm ] = useState({
error: {},
data: {}
});
useEffect( () => {
async function fetchData() {
const promise = await fetch(`test.json`);
const result = await promise.json();
const newForm = {...form};
newForm.data = result;
setForm(newForm);
console.log('executed');
}
fetchData();
}, []); // *** I will speak to this [] second argument shortly in question below
return (
<div>
<p>{Object.keys(form.data).length}</p>
</div>
);
};
All it does is on component mount, grab a test.json
file that has the content {"data":"hello"}
. This works perfectly and does what I want.
However, in my console, I see the compiler complain with this message Line 20:6: React Hook useEffect has a missing dependency: 'form'. Either include it or remove the dependency array react-hooks/exhaustive-deps
. When I add [ form ]
as the second argument to useEffect
or if I delete the []
second argument from useEffect
, then the useEffect
goes into infinite loop.
Why is my compiler warning me of an issue and suggesting an action that causes an infinite loop?
Upvotes: 1
Views: 576
Reputation: 2379
This error / warning is created by your linter.
The linter rule assumes that you have missed a variable that is external to the useEffect
in the dependency array which would cause unexpected outcomes.
You could disable the lint rule for:
useEffect(() => {
}, []); // eslint-disable-line react-hooks/exhaustive-deps
/* eslint-disable react-hooks/exhaustive-deps */
useEffect(() => {
}, []);
If you don't want to disable the rule you could swap to using the setState
callback syntax which provides the current state as a parameter.
import { useState, useEffect } from 'react';
const Main = () => {
const [ form, setForm ] = useState({
error: {},
data: {}
});
useEffect( () => {
async function fetchData() {
const promise = await fetch(`test.json`);
const result = await promise.json();
setForm(currentForm => {
const newForm = {...currentForm};
newForm.data = result;
return newForm;
});
console.log('executed');
}
fetchData();
}, []);
return (
<div>
<p>{Object.keys(form.data).length}</p>
</div>
);
};
This removes the need to include form
in the useEffect.
As to the reason why the linter might think this is an issue, look at this example:
export default function App() {
const [data, setData] = useState({ a: 'Initial Value', b: null });
const [control, setControl] = useState({ a: "Initial Value", b: null });
useEffect(() => {
const asyncFunc = () => {
new Promise(resolve => {
setTimeout(() => resolve(true), 2000)
})
.then(() => {
// The value of "data" will be the initial value from
// when the useEffect first ran.
setData({...data, b: 'Async Updated'});
// The value of "current" wille be the current value of
// the "control" state.
setControl(current => ({ ...current, b: "Async Updated" }));
})
};
asyncFunc();
// Update the data state while the async function has not
// yet been completed.
setData({ a: 'Changed Value', b: null });
// Update the control state while the async function has not
// yet been completed.
setControl({ a: "Changed Value", b: null });
}, []);
// The data value will swap to "Changed Value" and then back
// to "Initial Value" (unexpected) once the async request is
// complete.
// As the control used the current value provided by the
// callback it is able to avoid this issue.
return (
<div>
Data:
<pre>{JSON.stringify(data, null, "\t")}</pre>
Control:
<pre>{JSON.stringify(control, null, "\t")}</pre>
</div>
);
};
You can run this example here: https://stackblitz.com/edit/react-hmfddo
Upvotes: 4