react hooks (useEffect) infinite loop clarification - reactjs

This question is related to this previous question of mine. I have written two custom hooks. The first is useFetchAccounts whose job is to fetch some data about user accounts and looks like this:
// a fake fetch for the sake of the example
function my_fetch(f, delay) {
setTimeout(f, delay)
}
function useFetchAccounts() {
const [data, setData] = useState({accounts: [], loaded: false})
useEffect(() => {
my_fetch(() => {setData({accounts: [{id:1},{id:2}], loaded: true})}, 3000)
}, [])
return data
}
Nothing special about this hook, simply fetches the ids of the accounts.
Now I have another hook, useFetchBalancesWithIDs(account_ids, accounts_loaded), which is meant to take the ids from the previous step and fetch the balances of these accounts if the accounts have been loaded. It looks like this:
function useFetchBalanceWithIDs(account_ids, accounts_loaded) {
const [balances, setBalances] = useState(null)
useEffect(() => {
if (!accounts_loaded) return; // only fetch if accounts are loaded
my_fetch(() => {
setBalances(account_ids.map(id => 42+id))
}, 3000)
}, [account_ids, accounts_loaded])
return balances
}
As you can see if the accounts_loaded is false, it will not perform the fetch. Together I am using them as follows:
function App() {
const account_data = useFetchAccounts()
const accounts = account_data.accounts
const account_ids = accounts.map(account => account.id) // extract ids
const balance = useFetchBalanceWithIDs(account_ids, account_data.loaded)
console.log(balance)
return null
}
Unfortunately, this results in an infinite loop. What does work is changing the useFetchBalancesWithIDs to this:
function useFetchBalanceWithAccounts(accounts, accounts_loaded) {
const [balances, setBalances] = useState(null)
useEffect(() => {
if (!accounts_loaded) return; // only fetch if accounts are loaded
my_fetch(() => {
setBalances(accounts.map(account => 42+account.id))
}, 3000)
}, [accounts, accounts_loaded])
return balances
}
which performs the ids extraction within. I'm using it like this:
function App() {
const account_data = useFetchAccounts()
const accounts = account_data.accounts
const balance = useFetchBalanceWithAccounts(accounts, account_data.loaded)
console.log(balance)
return null
}
which runs just fine. So the problem seems to be related to the extraction of the IDs from within the App component, which seems to be triggering the useFetchBalanceWithIDs all the time, even though the account_ids do not change value. Can someone help explain this behaviour please? It's OK for me to use useFetchBalanceWithAccounts but I would like to understand why useFetchBalanceWithIDs doens't work. Thank you!

The issue is that you're using the map function to get an array of your account ids. React uses Object.is to determine if your values in the dependency array has changed in each render cycle. The map function returns a new array, so even though the values in your array are the same, the comparison check evaluates to false, and the effect is run.

Referential equality is used by useEffect, that means that [1,2,3] would not equal [1,2,3]
const data = [{id:1}];
data.map(d=>d.id)===data.map(d=>d.id);//is false
One way to prevent the array from id's from changing reference is combining useState with useEffect in App:
function App() {
//store account id's in app state
const [accountIds, setAccountIds] = useState([]);
const account_data = useFetchAccounts();
const accounts = account_data.accounts;
//only if accounts change re set the id's
useEffect(() => {
setAccountIds(accounts.map(a => a.id));
}, [accounts]);
const balance = useFetchBalanceWithAccounts(
accountIds,
account_data.loaded
);
console.log(balance);
return null;
}

Related

react query returning empty object for data

I am trying to abstract away my react/tanstack query.
I have a custom hook like the following:
const useGamesApi = () => {
const upcomingGamesQuery = useQuery(
["upcoming", date],
async () => {
const ret = await apiGetUpcomingGames(date);
return ret;
},
{
onSuccess: (data) => {
setGames(data);
},
}
);
return {
games: upcomingGamesQuery,
};
};
export default useGamesApi;
I am trying to consume my API as follows:
const [games, setGames] = useState<Game[]>([]);
const gamesApi = useGamesApi();
useEffect(() => {
setGames(gamesApi.games.data);
}, []);
This leads to compilation errors and also the value of my games state variable remains an empty array, as if the useEffect never ran.
Basically I am trying to abstract away my react query to provide a simplified way of interacting with it for my components, whilst also giving it a chance to modify the parameter of the date, so that I can be able to set until which date I would like to query.
What would be the correct (compilation vise) and idiomatic way of doing this with react?
(note I am using this in a react native project, not sure if it counts.)
As per rules , You need to add all the variables used inside useEffect as dependency so that it reacts once the value is changed.
You don't really need useEffect for you scenario. It is used to cause side effects. simply do it like :
const games: Game[] = gamesApi?.games?.data;
const games: Game[] = gamesApi?.games?.data || []; // incase you need default value

Nextjs/React : Skip useEffect function on particular state change

I built chart view page which display sales data.
This chart will updated through REST API when user change dimension, filter value and period.
useEffect(async () => {
let [startDate, endDate] = getReportPeriod({target: { innerText: period}}) // get formatted date string
getReportData(startDate, endDate) // function that call REST API to get chart data
}, [filteredDimensions, dimensions, dimensionTime])
Also, user can save and load chart setting.
const loadCustomReport = async(reportName) => {
let res = await axios.post('/api/customReport/loadReport', {
report_name: reportName
}) // get report setting
let setting = JSON.parse(res.data[0].report_setting) // parse report setting as json
setRevenue(setting.metrics.revenue)
setImpression(setting.metrics.impression)
setClicks(setting.metrics.clicks)
setCPM(setting.metrics.CPM)
setCTR(setting.metrics.CTR)
setDimensions(setting.dimensions)
setFilteredDimensions(setting.filteredDimensions)
}
The problem is the useEffect function run twice(both of setDimensions and setFilteredDimensions) and response times vary, so sometimes I got report data that a setting is not fully applied.
Is there any way to skip useEffect function running on setDimensions?
I think put both dimension setting and filter setting on one state might be works but I just curious that there are other solutions.
Please advise me any suggestion to solve the problem.
#Gabriele Petrioli give me the solution actually I wanted.
#man.in.the.jukebox and #Brandon Pelton-Cox's advice give me a great point.
Currently my code is as below(I changed before I saw #Gabriele Petrioli's answer)
useEffect(async () => {
let [startDate, endDate] = getReportPeriod({target: { innerText: period}})
getReportData(startDate, endDate)
}, [filteredDimensions, dimensionTime])
// button handler that add dimension to current dimensions state
const addDimension = (dimension) => {
setDimensions([
...dimensions,
dimension
])
let [startDate, endDate] = getReportPeriod({target: { innerText: period}})
getReportData(startDate, endDate)
}
// button handler that remove dimension from current dimensions state
const removeDimension = (idx) => {
let tempDimensions = [...dimensions]
tempDimensions.splice(idx, 1)
setDimensions(tempDimensions)
let [startDate, endDate] = getReportPeriod({target: { innerText: period}})
getReportData(startDate, endDate)
}
// load report setting
const loadCustomReport = async(reportName) => {
let res = await axios.post('/api/customReport/loadReport', {
report_name: reportName
})
let setting = JSON.parse(res.data[0].report_setting)
setRevenue(setting.metrics.revenue)
setImpression(setting.metrics.impression)
setClicks(setting.metrics.clicks)
setCPM(setting.metrics.CPM)
setCTR(setting.metrics.CTR)
setDimensions(setting.dimensions)
setFilteredDimensions(setting.filteredDimensions)
}
Thank you all for give me great advices.
Since your state updates are fired from an async function, react does not batch them by default
(applies to React versions before 18, as in 18 all updates are batched regardless of where they originate).
You need to opt-in to unstable_batchedUpdates, so
const loadCustomReport = async(reportName) => {
let res = await axios.post('/api/customReport/loadReport', {
report_name: reportName
}) // get report setting
let setting = JSON.parse(res.data[0].report_setting) // parse report setting as json
React.unstable_batchedUpdates(() => {
setRevenue(setting.metrics.revenue)
setImpression(setting.metrics.impression)
setClicks(setting.metrics.clicks)
setCPM(setting.metrics.CPM)
setCTR(setting.metrics.CTR)
setDimensions(setting.dimensions)
setFilteredDimensions(setting.filteredDimensions)
});
}
By including dimensions in the dependency array for the useEffect function, you are telling React to re-render the page and execute the function in that useEffect block whenever setDimensions is called. Therefore, if you want to prevent that function from being executed when dimensions is updated, you need to remove from the dependencies array, like so
useEffect(async () => {
...
}, [filteredDimensions, dimensionTime])

Why is useState value null inside function block?

I know there is a scoping issue here. I just can't find it. Why is 'items' null in the searchItems() block?
export const useStore = () => {
const [items, setItems] = useState(null)
const setItemsFromApi = () => {
//call api to get data, then
setItems(data)
}
const searchItems = (query) => {
//use the local data and filter based on query
//'items' IS NULL IN THIS SCOPE
items.filter(() => {})
}
console.log(items) // 'items' HAS UPDATED VALUE AFTER setItemsFromApi() IN THIS SCOPE
return {setItemsFromApi, searchItems}
}
Use store like this. (NOTE: I left out the rendering of the items in the list because that part works fine. Just focusing on why the onClick doesn't use the already loaded items to filter with.)
export default function DataList(props) => {
const store = useStore();
useEffect(() => {
store.setItemsFromApi()
}, [])
const runSearch = (query) => {
store.searchItems(query)
}
return <button onClick={runSearch('searchTerm')}
}
I even tried passing it as a callback dependency, but it's still null
const searchItems = useCallback((query) => {
//'items' IS STILL NULL IN THIS SCOPE
items.filter(() => {})
}, [items])
From the code you posted,
const store = useStore()
store.setItemsFromApi()
...
store.searchItems(query)
the issue may be because you are doing an async operation (calling the API), but the search is not waiting for the result of the fetch call. So, when you do the
store.searchItems(query)
, the store is null and only changes its value later.
In a nutshell, the state wasn't refreshing after triggering a search because I had a "debounce" useRef function running within the component when the onChange event was fired, even though this was a local data search. I guess this interrupted the re-render event. So I removed it.

In React, fetch data conditional on results of an initial fetch

We have written a custom data fetching hook useInternalApi which is similar to the useDataApi hook at the very bottom of this fairly decent tutorial on data fetching with react hooks. Our app fetches a lot of sports data, and in particular, we are trying to figure out the right data-fetching pattern for our use case, which is fairly simple:
Fetch general info for a specific entity (an NCAA conference, for example)
Use info returned with that entity (an array of team IDs for teams in the specific conference), and fetch info on each team in the array.
For this, our code would then look something like this:
import `useInternalApi` from '../path-to-hooks/useInternalApi';
// import React... and other stuff
function ComponentThatWantsTeamInfo({ conferenceId }) {
// use data fetching hook
const [conferenceInfo, isLoading1, isError1] = useInternalApi('conferenceInfo', { conferenceId: conferenceId })
// once conferenceInfo loads, then load info from all teams in the conference
if (conferenceInfo && conferenceInfo.teamsArray) {
const [teamInfos, isLoading2, isError2] = useInternalApi('teamInfo', { teamIds: conferenceInfo.teamIds })
}
}
In the example above, conferenceId is an integer, teamIds is an array of integers, and the combination of the 2 parameters to the useInternalApi function create a unique endpoint url to fetch data from. The two main problems with this currently are:
Our useInternalApi hook is called in an if statement, which is not allowed per #1 rule of hooks.
useInternalApi is currently built to only make a single fetch, to a specific endpoint. Currently, it cannot handle an array of teamIds like above.
What is the correct data-fetching pattern for this? Ideally, teamInfos would be an object where each key is the teamId for one of the teams in the conference. In particular, is it better to:
Create a new internal hook that can handle an array of teamIds, will make the 10 - 20 fetches (or as many as needed based on the length of the teamsArray), and will use Promise.all() to return the results all-together.
Keep the useInternalApi hook as is, and simply call it 10 - 20 times, once for each team.
Edit
I'm not sure if the underlying code to useInternalApi is needed to answer this question. I try to avoid creating very long posts, but in this instance perhaps that code is important:
const useInternalApi = (endpoint, config) => {
// Set Data-Fetching State
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
// Use in lieu of useEffect
useDeepCompareEffect(() => {
// Token/Source should be created before "fetchData"
let source = axios.CancelToken.source();
let isMounted = true;
// Create Function that makes Axios requests
const fetchData = async () => {
// Set States + Try To Fetch
setIsError(false);
setIsLoading(true);
try {
const url = createUrl(endpoint, config);
const result = await axios.get(url, { cancelToken: source.token });
if (isMounted) {
setData(result.data);
}
} catch (error) {
if (isMounted) {
setIsError(true);
}
} finally {
if (isMounted) {
setIsLoading(false);
}
}
};
// Call Function
fetchData();
// Cancel Request / Prevent State Updates (Memory Leaks) in cleanup function
return () => {
isMounted = false; // set to false to prevent state updates / memory leaks
source.cancel(); // and cancel the http request as well because why not
};
}, [endpoint, config]);
// Return as length-3 array
return [data, isLoading, isError];
};
In my opinion, if you need to use a hook conditionally, you should use that hook inside of a separate component and then conditionally render that component.
My understanding, correct me if I'm wrong, is that the initial API call returns an array of ids and you need to fetch the data for each team based on that id?
Here is how I'd do something of that sorts.
import `useInternalApi` from '../path-to-hooks/useInternalApi';
// import React... and other stuff
function ComponentThatDisplaysASpecificTeam(props){
const teamId = props.teamId;
const [teamInfo] = useInternalApi('teamInfo', { teamId });
if(! teamInfo){
return <p>Loading...</p>
}
return <p>do something with teamInfo...</p>
}
function ComponentThatWantsTeamInfo({ conferenceId }) {
// use data fetching hook
const [conferenceInfo, isLoading1, isError1] = useInternalApi('conferenceInfo', { conferenceId: conferenceId })
if (! conferenceInfo || ! conferenceInfo.teamsArray) {
return <p>this is either a loading or an error, you probably know better than me.</p>
}
// Let the data for each team be handled by its own component. This also lets you not have to use Promise.all
return (
<div>
{conferenceInfo.teamIds.map(teamId => (
<ComponentThatDisplaysASpecificTeam teamId={teamId} />
))}
</div>
)
}

Correct way to update react component from secondary source

I'm kind of new with React Hooks and I've encountered a problem when making a component. My App has a simple form with a few fields and a "Calculate" button which fetches info from an API and displays the results on a table. The app uses two currencies, they can be switched with a pair of buttons. What I want is to update the table(re fetch the data) when currency is changed, but only is there was already something calculated via the main "Calculate" button before changing the currency. My component is something along the lines of:
const ProductionCosts = () => {
const [data, setData] = useState({});
const [useXCurrency, setUseXCurrency] = useState(true);
const calcCosts = useCallback(async () => {
fetchCalcData(args);
}, [args]);
useEffect(() => {
if (Object.keys(data).length > 0) //check data isn't empty, hence it was already calculated
fetchCalcData();
}, [useXCurrency]);
return (
......
);
};
Doing something similar to the above works, but the linter will say that data needs to be in the dependency list of the useEffect, but adding it will result on a loop given that fetchCalcData modifies data and triggers the effect, I DO know that the linter suggestions aren't absolute, but at the same time I know that there must be a better way. So besides adding Boolean flags or something like that, there is a better approach to this case?
Typically you want to use a refenence with the initial value and update it on success, on next useEffect the condition will be falsy:
const ProductionCosts = () => {
const [data, setData] = useState({});
const dataRef = useRef(data);
useEffect(() => {
if (Object.keys(dataRef.current).length > 0) {
const data = // fetch your data
dataRef.current = data;
}
}, [useXCurrency]);
return <></>;
};

Resources