Using useState inside custom react hooks causes renderings - reactjs

I created a custom react hook to fetch data. Unfortunately when the useGetData gets called from a component, the component will render for each useState that is performed inside the hook. How can I prevent the additional renderings?
export default function useGetData(
setData: (fetchData) => void
): [(id: string) => void, boolean, boolean] {
const [loadingData, setLoading] = useState(false)
const [successData, setSuccess] = useState(false)
const getData = (id: string) => {
if (!id || !Number(id)) {
setData(null)
return
}
setSuccess(false)
setLoading(true)
Api.getData(Number(id))
.then((response) => {
setSuccess(true)
setData(response)
})
.finally(() => {
setLoading(false)
})
}
return [getData, loadingData, successData]
}

React < 18.x does not do automatical batching for promise callbacks, timeouts, intervals, .... It does it only for event handlers registered via JSX props and in lifecycle methods / effects.
Solution 1: You can use "unstable" API ReactDOM.unstable_batchedUpdates which, despite being marked as unstable, has been working for years just fine:
Api.getData(Number(id))
.then((response) => ReactDOM.unstable_batchedUpdates(() => {
setSuccess(true)
setData(response)
}))
.finally(() => {
setLoading(false)
})
The function will batch all state updates that are executed inside of passed function (synchronous); in the example setSuccess and setData; so that that they will be merged into single re-render, just like React does it in onClick and other event handlers.
Other possibilities: I can think of 2 other options that I don't really like but might work for you:
You can merge all your state into single state variable so that you have single setter.
Instead of calling setSuccess(true); setData(response); in promise callback, you can do it in an effect by introducing another state variable.
const [response, setResponse] = useState();
useEffect(() => {
if (!response) return;
setSuccess(true);
setData(response);
}, [response]);
...
Api.getData(Number(id))
.then(setResponse)
.finally(() => {
setLoading(false)
})

Related

React- Issue with infinite loop in useEffect

I have a useEffect() that fetches the data through axios, I want that to render only one time so I passed an array. Everything works fine, but the problem is whenever I try to sort the items, the second useEffect fires just followed by the first useEffect, which is causing the component to fetch the items all over again and again.
const [products, setProducts] = useState([]);
useEffect(() => {
const getProducts = async () => {
return await axios
.get('/getAllProducts')
.then((response) => {
setProducts(response.data);
console.log(products);
})
.catch((e) => {
console.log(e);
});
};
getProducts();
}, [products]);
This is because you passed an array containing your products state, rather than an empty array, which will fire useEffect on state change (for products state specifically). Try changing your code to an empty array:
useEffect(() => {
const getProducts = async () => {
return await axios
.get('/getAllProducts')
.then((response) => {
setProducts(response.data);
console.log(products);
})
.catch((e) => {
console.log(e);
});
};
getProducts();
}, []);
As #skyboyer mentioned below, it is good to note that state is not updated in a synchronous manner. Therefor, console.log(products) will not reflect an accurate value for your state when useEffect runs.
It is okay to use multiple useEffect hooks. If you would like to view your updated state in the console, or do some other work with it, you could add another useEffect hook and pass your state into the array:
useEffect(() => {
console.log(products);
}, [products]);
Since products is in the useEffect dependency array, it is going to run every time there are changes made to the products state. getProducts() runs setProducts which then in turn is going to trigger the use effect again. Using an empty array in the useEffect will tell it to only run when the component is mounted.
Like this:
useEffect(() => {
const getProducts = async () => {
return await axios
.get('/getAllProducts')
.then((response) => {
setProducts(response.data);
console.log(products);
})
.catch((e) => {
console.log(e);
});
};
getProducts();
}, []);

useEffect() not receiving prop

I'm semi-new to React and learning.
I implemented a useEffect() hook as follows:
import { fetchDetails } from "../../ApiServices/Details";
export interface RemoveModalProps {
id: number;
isVisible: boolean;
onHide: () => void;
handleRemove: (resp: JsonResponseModel<ApiResponse>) => void;
}
const RemoveModal= ({ id, isVisible, onHide, handleRemove }: RemoveModalProps) => {
const [details, setDetails] = React.useState<DetailsModel>();
React.useEffect(() => {
console.log(id);
async function getDetails() {
var details= await fetchDetails(id);
console.log(details.Data);
setDetails(details.Data);
}
getDetails();
}, []);
However, for reasons I'm not quite able to understand, the console.log(id) is returning null. However, I have the following function just below the useEffect()
const handleSubmit = async () => {
const isValid = formRef.current.checkValidity();
setValidated(true);
if (isValid) {
console.log(id);
setSaving(true);
const resp = await removeDetails(id);
}
};
and THIS console.log(id) is logging the correct id, so obviously the id is being properly passed as a prop. What am I doing wrong with my useEffect hook that it's not able to use the prop?
useEffect accepts array of dependencies as second parameter. changes in values for dependencies will trigger function in side useeffect again and again.
In your case you need id as dependant value in useEffect. So
useEffect needs to look like this
React.useEffect(() => {
console.log(id);
async function getDetails() {
var details= await fetchDetails(id);
console.log(details.Data);
setDetails(details.Data);
}
getDetails();
}, [id]);
You may need to handle null/undefined value in id. I suggest you to do something like this.
React.useEffect(() => {
if (!id) {
return
}
... rest of hook
}, [id]);
I suggest you to use eslint to warn about useEffect missing dependencies.
References :
useEffect documenation

How do I fetch data and use it in useEffect()?

I'm trying to fetch data from an API and set a state with the data, but when I use the data in a child component, I get an [Unhandled promise rejection: TypeError: null is not an object (evaluating 'data.name')] warning.
Here is a gist of what I'm trying to do. Does anyone know why this might be occurring? I assume it's because the data isn't received from the API. I have tried adding an "isLoading" state and only returning the ChildComponent if it's false, but I still get the same warning (this might be because setNewProp in useEffect isn't updating when it receives the data from the API).
const ParentComponent = (props) => {
const [data, setData] = useState(null);
const [newProp, setNewProp] = useState();
const fetchData = async () => {
new DataService.retrieveData().then((response) => {
setData(response);
}
}
useEffect(() => {
fetchData();
setNewProp({ data, ...props });
}, []);
return (
<ChildComponent newProp={newProp} />
);
}
You cannot use an async function inside an useEffect lifecycle event. As a good solution i would recommend to fully utilize the useEffect hook and use it as an effect to the updated data.
const ParentComponent = (props) => {
const [data, setData] = useState(null);
const [newProp, setNewProp] = useState();
const fetchData = async () => {
new DataService.retrieveData().then((response) => {
setData(response);
}
}
useEffect(() => {
fetchData();
}, []);
useEffect(() => {
setNewProp({ data, ...props });
}, [data]);
return (
<ChildComponent newProp={newProp} />
);
}
I also want to point out that useEffect runs AFTER the first render. That means your ChildComponent will always receive "undefined" as first props, since there is no initial value set at:
const [newProps, setNewProp] = useState(); // initial value comes here to prevent errors
Looks like maybe you have missed the await that is needed in useEffect() to make the code wait until that fetch has finished:
Before:
useEffect(() => {
fetchData();
setNewProp({ data, ...props });
}, []);
After:
useEffect(() => {
(async () => {
await fetchData();
setNewProp({ data, ...props });
})();
}, []);
Note that useEffect() doesn't support async functions (because it needs the return value to be a cleanup function, or undefined. For example, see this article.
BUT even better might be something like:
const [data, setData] = useState(null);
const fetchData = async () => {
new DataService.retrieveData().then((response) => {
setData(response);
}
}
fetchData();
if (data) {
const newProp = { data, ...props };
}
In your code, you first call fetchData function, which calls a useState hook when it's done. Since useState hook works asynchronously, the state data will not be changed right after.
useEffect(() => {
fetchData(); // Called setData()
setNewProp({ data, ...props }); // At this point, data hasn't changed yet.
}, []);
So you can use useEffect hook again to watch for changes in your data state. Then you should set the new state of your newProp.
useEffect(() => {
(async () => {
await fetchData();
})();
}, []);
useEffect(() => {
setNewProp({...props, data });
}, [data]);

useEffect missing dependency when using redux useDispatch

I wanna fetch my categories whenever my component is mounted using react hooks useEffect and not on every re-render. But i keep on getting this warning React Hook useEffect has a missing dependency:'dispatch'.
Here's my code:
const categories = useSelector(state => state.category.categories);
const dispatch = useDispatch();
useEffect(() => {
console.log('effecting');
const fetchCategories = async () => {
console.log('fetching');
try {
const response = await axios.get('/api/v1/categories');
dispatch(initCategory(response.data.data.categories));
} catch (e) {
console.log(e);
}
}
fetchCategories();
}, []);
You can safely add the dispatch function to the useEffect dependency array. If you look the react-redux documentation, specially the hooks section, they mention this "issue".
The dispatch function reference will be stable as long as the same
store instance is being passed to the . Normally, that store
instance never changes in an application.
However, the React hooks lint rules do not know that dispatch should
be stable, and will warn that the dispatch variable should be added to
dependency arrays for useEffect and useCallback. The simplest solution is to do just that:
export const Todos() = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchTodos())
// Safe to add dispatch to the dependencies array
}, [dispatch])
}
Add dispatch to your dependency array (which is currently empty).
useEffect(() => {
console.log('effecting');
const fetchCategories = async () => {
console.log('fetching');
try {
const response = await axios.get('/api/v1/categories');
dispatch(initCategory(response.data.data.categories));
} catch (e) {
console.log(e);
}
}
fetchCategories();
}, [dispatch]);
It might be a problem with ignored promise.
fetchCategories() returns a promise.
You can try
useEffect(() => {
const fetchCategories = async () => {
try {
const response = await axios.get('/api/v1/categories');
await dispatch(initCategory(response.data.data.categories));
} catch (e) {
console.log(e);
}
}
fetchCategories().then(res => (console.log(res());
}, []);

Hooks in react are not updating my state correctly

I'm trying to load some data which I get from an API in a form, but I seem to be doing something wrong with my state hook.
In the code below I'm using hooks to define an employee and employeeId.
After that I'm trying to use useEffect to mimic the componentDidMount function from a class component.
Once in here I check if there are params in the url and I update the employeeId state with setEmployeeId(props.match.params.employeeId).
The issue is, my state value didn't update and my whole flow collapses.
Try to keep in mind that I rather use function components for this.
export default function EmployeeDetail(props) {
const [employeeId, setEmployeeId] = useState<number>(-1);
const [isLoading, setIsLoading] = useState(false);
const [employee, setEmployee] = useState<IEmployee>();
useEffect(() => componentDidMount(), []);
const componentDidMount = () => {
// --> I get the correct id from the params
if (props.match.params && props.match.params.employeeId) {
setEmployeeId(props.match.params.employeeId)
}
// This remains -1, while it should be the params.employeeId
if (employeeId) {
getEmployee();
}
}
const getEmployee = () => {
setIsLoading(true);
EmployeeService.getEmployee(employeeId) // --> This will return an invalid employee
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}
return (
<div>
...
</div>
)
}
The new value from setEmployeeId will be available probably in the next render.
The code you're running is part of the same render so the value won't be set yet.
Since you're in the same function, use the value you already have: props.match.params.employeeId.
Remember, when you call set* you're instructing React to queue an update. The update may happen when React decides.
If you'd prefer your getEmployee to only run once currentEmployeeId changes, consider putting that in its own effect:
useEffect(() => {
getEmployee(currentEmployeeId);
}, [currentEmployeeId])
The problem seems to be that you are trying to use the "updated" state before it is updated. I suggest you to use something like
export default function EmployeeDetail(props) {
const [employeeId, setEmployeeId] = useState<number>(-1);
const [isLoading, setIsLoading] = useState(false);
const [employee, setEmployee] = useState<IEmployee>();
useEffect(() => componentDidMount(), []);
const componentDidMount = () => {
// --> I get the correct id from the params
let currentEmployeeId
if (props.match.params && props.match.params.employeeId) {
currentEmployeeId = props.match.params.employeeId
setEmployeeId(currentEmployeeId)
}
// This was remaining -1, because state wasn't updated
if (currentEmployeeId) {
getEmployee(currentEmployeeId);
//It's a good practice to only change the value gotten from a
//function by changing its parameter
}
}
const getEmployee = (id: number) => {
setIsLoading(true);
EmployeeService.getEmployee(id)
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}
return (
<div>
...
</div>
)
}
The function returned from useEffect will be called on onmount. Since you're using implicit return, that's what happens in your case. If you need it to be called on mount, you need to call it instead of returning.
Edit: since you also set employee id, you need to track in the dependency array. This is due to the fact that setting state is async in React and the updated state value will be available only on the next render.
useEffect(() => {
componentDidMount()
}, [employeeId]);
An alternative would be to use the data from props directly in the getEmployee method:
useEffect(() => {
componentDidMount()
}, []);
const componentDidMount = () => {
if (props.match.params && props.match.params.employeeId) {
setEmployeeId(props.match.params.employeeId)
getEmployee(props.match.params.employeeId);
}
}
const getEmployee = (employeeId) => {
setIsLoading(true);
EmployeeService.getEmployee(employeeId);
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}

Resources