Async/Await Function keeps triggering re-rendering - reactjs

I having problem trying to get data from backend using axios. The function returns a Promise and whenever I call the function my component keeps rendering non-stop. Here is the code.
import { useState } from "react";
import Axios from "axios";
const DashBoard = () => {
const [student, setStudent] = useState<{ [key: string]: string }[]>([]);
const studentId = JSON.parse(localStorage.getItem("studentId") as string);
const examResult: { [key: string]: string }[] = JSON.parse(
localStorage.getItem("englishAnswers") as string
);
const getStudent = async () => {
const { data } = await Axios.get(
`http://localhost:3500/students/${studentId}`
);
setStudent(data);
};
getStudent(); //this line here keeps triggering re-render non stop;

The function getStudent is being invoked on every render:
getStudent();
Since the operation within that function updates state, it triggers a re-render. So every render triggers a re-render, indefinitely.
If the intent is only to execute this on the first render, wrap it in a useEffect with an empty dependency array:
useEffect(() => {
getStudent();
}, []);
If studentId might change and you want to re-invoke this if it does, add that to the dependency array:
useEffect(() => {
getStudent();
}, [studentId]);
You might take it a step further and put the function itself into a useCallback:
const getStudent = useCallback(async () => {
const { data } = await Axios.get(
`http://localhost:3500/students/${studentId}`
);
setStudent(data);
}, [studentId]);
This will only create a new instance of the function if/when studentId changes. Then the function itself can be the dependency for executing it:
useEffect(() => {
getStudent();
}, [getStudent]);

You need to wrap your getStudent() call in an useEffect hook. Because currently you are calling the getStudent function on each render, and as it triggers a setState method this leads into an infinite rendering.
useEffect(() => {
const getStudent = async () => {
const { data } = await Axios.get(
`http://localhost:3500/students/${studentId}`
);
setStudent(data);
};
getStudent();
}, []);

Related

React Hook UseEffect cannot be called at the top level

I am getting this same error whether I use useEffect inside of my function component or out of it:
React Hook "useEffect" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function
import React, { useEffect } from "react";
const GetVerse = async () => {
useEffect(async () => {
const fetchVerse = await fetch(
"https://bhagavad-gita3.p.rapidapi.com/v2/chapters/1/verses/null/"
);
const verseBody = await fetchVerse.json();
console.log(verseBody);
});
return <div></div>;
};
export default GetVerse;
You component can't be async. But if you want to have an async useEffect, you have to do like this :
const GetVerse = () => { // component can't be async
useEffect(() => { // useEffect callback can't be async either
const fn = async () => { // declare an async function here
const fetchVerse = await fetch(url)
const verseBody = await fetchVerse.json()
console.log(verseBody);
}
fn() // call the async function
}, []) // don't forget to add dependency array here
return <div></div>
}
React Components and the useEfffect callback shouldn't be async. Here is a way to use await in the callback. The idea is to create a async function and then call it.
Also consider adding an empty array [] as the second parameter to useEffect as not putting this will cause infinite API calls.
import React, { useEffect } from "react";
const GetVerse = () => {
useEffect(() => {
async function getData() {
const fetchVerse = await fetch(
"https://bhagavad-gita3.p.rapidapi.com/v2/chapters/1/verses/null/"
);
const verseBody = await fetchVerse.json();
console.log(verseBody);
}
getData();
}, []);
return <div></div>;
};
export default GetVerse;

ReactJS - use localStorage as a dependency for useEffect causes infinite loop

This code give me infinite loop at line console.log
const userInfo = JSON.parse(localStorage.getItem("user_info"));
const [filterSemester, setFilterSemester] = useState(SEMESTERS[0]);
const [scoreData, setScoreData] = useState(null);
useEffect(() => {
getData();
}, [userInfo, filterSemester]);
useEffect(() => {
console.log("scoreData: ", scoreData);
}, [scoreData]);
const getData = () => {
const params = {
student_id: userInfo?.student_info?.id,
school_year_id:
userInfo?.student_info?.class_info?.grade_info?.school_year_id,
semester: filterSemester.key,
};
getStudyInfoBySchoolYear(params).then((res) => {
if (res?.status === 200) {
setScoreData(res?.data?.data);
}
});
};
If I remove userInfo from the dependency array of the first useEffect, the loop will gone, I wonder why? I didn't change it at all in the code.
userInfo is actually changing.
It is a functional component, so all the code that is inside the component will run on every render, thus, userInfo gets re-created on every render, because it was not declared as a reference (with useRef) or, more commonly, as a state (with useState).
The flow is as follows:
The component mounts.
The first useEffect runs getData. The second useEffect also runs.
getData will update scoreData state with setScoreData. This latter will trigger a re-render, and also scoreData has changed, so the second useEffect will run.
When the render takes place, all the code within your component will run, including the userInfo declaration (creating a new reference to it, unless localStorage.getItem("user_info") is returning undefined).
React detects userInfo as changed, so the first useEffect will run again.
The process repeats from step 3.
You could replace your
const userInfo = JSON.parse(localStorage.getItem("user_info"));
with
const userInfo = React.useRef(JSON.parse(localStorage.getItem("user_info")));
and your
useEffect(() => {
getData();
}, [userInfo, filterSemester]);
with
useEffect(() => {
getData();
}, [userInfo.current, filterSemester]);
try this
const userInfo = JSON.parse(localStorage.getItem("user_info"));
const [filterSemester, setFilterSemester] = useState(SEMESTERS[0]);
const [scoreData, setScoreData] = useState(null);
useEffect(() => {
getData();
}, [localStorage.getItem("user_info"), filterSemester]);
useEffect(() => {
console.log("scoreData: ", scoreData);
}, [scoreData]);
const getData = () => {
const params = {
student_id: userInfo?.student_info?.id,
school_year_id:
userInfo?.student_info?.class_info?.grade_info?.school_year_id,
semester: filterSemester.key,
};
getStudyInfoBySchoolYear(params).then((res) => {
if (res?.status === 200) {
setScoreData(res?.data?.data);
}
});
};

Why React detect the function reference as new?

I have theorical question about custom hooks and use effect when redux is involved.
Let`s assume I have this code:
//MyComponent.ts
import * as React from 'react';
import { connect } from 'react-redux';
const MyComponentBase = ({fetchData, data}) => {
React.useEffect(() => {
fetchData();
}, [fetchData]);
return <div>{data?.name}</data>
}
const mapStateToProps= state => {
return {
data: dataSelectors.data(state)
}
}
const mapDispatchToProps= {
fetchData: dataActions.fetchData
}
export const MyComponent = connect(mapStateToProps, mapDispatchToProps)(MyComponentBase);
This works as expected, when the component renders it does an async request to the server to fetch the data (using redux-thunk). It initializes the state in the reduces, and rerender the component.
However we are in the middle of a migration to move this code to hooks. Se we refactor this code a little bit:
//MyHook.ts
import { useDispatch, useSelector } from 'react-redux';
import {fetchDataAction} from './actions.ts';
const dataState = (state) => state.data;
export const useDataSelectors = () => {
return useSelector(dataState);
}
export const useDataActions = () => {
const dispatch = useDispatch();
return {
fetchData: () => dispatch(fetchDataAction)
};
};
//MyComponent.ts
export const MyComponent = () => {
const data = useDataSelectors()>
const {fetchData} = useDataActions();
React.useEffect(() => {
fetchData()
}, [fetchData]);
return <div>{data?.name}</data>
}
With this change the component enters in an infite loop. When it renders for the first time, it fetches data. When the data arrives, it updates the store and rerender the component. However in this rerender, the useEffect says that the reference for fetchData has changed, and does the fetch again, causing an infinite loop.
But I don't understand why the reference it's different, that hooks are defined outside the scope of the component, they are not removed from the dom or whateverm so their references should keep the same on each render cycle. Any ideas?
useDataActions is a hook, but it is returning a new object instance all the time
return {
fetchData: () => dispatch(fetchDataAction)
};
Even though fetchData is most likely the same object, you are wrapping it in a new object.
You could useState or useMemo to handle this.
export const useDataActions = () => {
const dispatch = useDispatch();
const [dataActions, setDataActions] = useState({})
useEffect(() => {
setDataActions({
fetchData: () => dispatch(fetchDataAction)
})
}, [dispatch]);
return dataActions;
};
first of all if you want the problem goes away you have a few options:
make your fetchData function memoized using useCallback hook
don't use fetchData in your useEffect dependencies because you don't want it. you only need to call fetchData when the component mounts.
so here is the above changes:
1
export const useDataActions = () => {
const dispatch = useDispatch();
const fetchData = useCallback(() => dispatch(fetchDataAction), []);
return {
fetchData
};
};
the 2nd approach is:
export const MyComponent = () => {
const data = useDataSelectors()>
const {fetchData} = useDataActions();
React.useEffect(() => {
fetchData()
}, []);
return <div>{data?.name}</data>
}

React Redux - useState Hook not working as expected

I have 2 actions in redux (both async) and I'm calling them both within my functional component via dispatch; the first using useEffect and the second via a button click. What I want to do is dispatch the actions to retrieve them from an async function, then use them within my component via useState. But using the useState is not rendering.
Here is my component:
export default function Hello()
{
const { first, second } = useSelector(state => state.myReducer);
const dispatch = useDispatch();
const fetchFirst = async () => dispatch(getFirst());
const fetchSecond = async () => dispatch(getSecond());
const fetchFixturesForDate = (date: Date) => dispatch(getFixturesForDate(date));
const [superValue, setSuperValue] = useState('value not set');
useEffect(() => {
const fetch = async () => {
fetchFirst();
setSuperValue(first);
};
fetch();
}, []);
const getSecondOnClickHandler = async () =>
{
console.log('a')
await fetchSecond();
setSuperValue(second);
}
return (
<div>
<p>The super value should first display the value "first item" once retrieved, then display "second value" once you click the button and the value is retrieved</p>
<p>Super Value: {superValue}</p>
<p>First Value: {first}</p>
<p>Second Value: {second}</p>
<button onClick={async () => await getSecondOnClickHandler()}>Get Second</button>
</div>
)
}
The superValue never renders even though I am setting it, although the value from first and second is retrieved and displayed.
StackBlitz.
Any help?
The value of first and second inside your two useEffects is set when the component mounts (I guess at that point they are undefined). So in both cases you will be setting superValue to that initial value.
You have two options:
Return the first/second values back from fetchFirst and fetchSecond, so that you can retrieve them directly from the executed function, and then set superValue:
useEffect(() => {
const fetch = async () => {
const newFirst = await fetchFirst();
setSuperValue(newFirst);
};
fetch();
}, []);
Add separate useEffects that listen for changes to first and second
useEffect(() => {
setSuperValue(first)
},[first])
useEffect(() => {
setSuperValue(second)
},[second])
The value in the reducer is not necessarily set when the action is dispatched, e.g. after fetchFirst() is called. Also the await that you do in await fetchSecond();
doesn't help since the reducer function is not executed.
You could add useEffect hooks and remove the setSuperValue from the other methods, but I think the code gets quite complicated.
What problem are you trying to solve in the first place?
useEffect(() => setSuperValue(first), [first]);
useEffect(() => setSuperValue(second), [second]);
useEffect(() => {
const fetch = async () => {
fetchFirst();
};
fetch();
}, []);
const getSecondOnClickHandler = async () => {
console.log('a');
await fetchSecond();
};
https://stackblitz.com/edit/react-ts-hsqd3x?file=Hello.tsx

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

Resources