I have a React component which receives data from a parent.
Now, this data is dynamic and I do not know beforehand what the properties are called exactly.
I need to render them in a certain fashion and that all works perfectly fine.
Now though, these dynamic objects have a property which is a number, which has to be displayed in my component.
To do so, I thought while iterating over the data, I will add the values to the sum, which is to be displayed. Whenever one of the data object changes, the sum will change, too (since I am using useState and React will detect that change.
But that exactly is the problem I don't know how to solve.
It is obvious that right now my code generates an infinite-loop:
The component is created and rendered for the first time.
During this process, setSum() is called, and therefore changing the state.
React detects that and orders a re-rendering.
So how do I fix this? I feel like I am missing something quite obvious here, but I am too invested to see it.
I have tried to boil down my code to the most easy to read code snippet which focuses on the problem only. Any suggestions to improve the readabilty are welcome!
const ComponentA = (data) => {
const [sum, setSum] = React.useState(0);
const renderData = (dataToRender) => {
//Here lies the problem already
setSum(0)
const result = [];
dataToRender.forEach((objData, index) => {
result.push(<JSX Item>Content</JSX Item>);
//and some more stuff, not relevant
// will not get this far
const newSum = sum+objData.propertyAmount;
setSum(newSum);
});
return result;
};
return(
//...someJSXElements
{data.relevantObjectArray && renderData(data.relevantObjectArray)}
<div>{sum}</div>
);
}
The reason it's re-rendering infinitely is because you are setting the state every time the component is rendered, which subsequently triggers another re-render. What you need to do is separate your display code from your state-setting code. I initially thought that useEffect would be a good solution (you can see the edit history for my original answer), however from the React docs:
useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. (React docs)
So you could therefore try something like this:
const reducer = (sum, action) => {
switch (action.type) {
case "increment":
return sum + action.propertyAmount;
default:
throw new Error();
}
};
const initialSum = 0;
const ComponentA = (data) => {
const [sum, sumReducer] = React.useReducer(reducer, initialSum);
React.useEffect(() => {
data.relevantObjectArray.forEach((objData, index) => {
sumReducer({ type: "increment", propertyAmount: objData.propertyAmount });
});
}, [data.relevantObjectArray]);
const renderData = (dataToRender) => {
const result = [];
dataToRender.forEach((objData, index) => {
result.push(<div>Content</div>);
});
return result;
};
return (
<div>
{data.relevantObjectArray && renderData(data.relevantObjectArray)}
<div>{sum}</div>
</div>
);
};
Example on codesandbox.io.
const ComponentA = (data) => {
const [sum, setSum] = React.useState(0);
const renderData = (dataToRender) => {
//Here lies the problem already
setSum(0)
const result = [];
dataToRender.forEach((objData, index) => {
result.push(<JSX Item>Content</JSX Item>);
//and some more stuff, not relevant
// will not get this far
const newSum = sum+objData.propertyAmount;
setSum(newSum);
});
return result;
};
React.useEffect(() => {
renderData();
return () => {
console.log('UseEffect cleanup')});
}, [data);
return(
//...someJSXElements
//The line below is causing the continuos re-render because you keep calling the function (renderData)
//{data.relevantObjectArray && renderData(data.relevantObjectArray)}
<div>{sum}</div>
);
}
Related
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.
I started learning react, yesterday I ran into this issue, somebody please explain me.
When I click button "add to wishlist" the piece of code below run 1st time, set product property "inWishList": true, but unknown why it rerun and set it back to "false" value.
const AddToWishList = (id) => {
setProducts((prev) => {
const latest_Products = [...prev];
const selected_Prod_Id = latest_Products.findIndex((x) => x.id === id);
latest_Products[selected_Prod_Id].inWishList =
!latest_Products[selected_Prod_Id].inWishList;
console.log(latest_Products);
return latest_Products;
});
};
_ The piece of code below works perfect, run only 1 time, however, i don't understand the difference between 2 codes
const AddToWishList = (id) => {
setProducts((currentProdList) => {
const prodIndex = currentProdList.findIndex((p) => p.id === id);
const newFavStatus = !currentProdList[prodIndex].inWishList;
const updatedProducts = [...currentProdList];
updatedProducts[prodIndex] = {
...currentProdList[prodIndex],
inWishList: newFavStatus,
};
console.log(updatedProducts);
return updatedProducts;
});
};
In the first snippet you are mutating the state object when toggling the inWishList property.
const AddToWishList = (id) => {
setProducts((prev) => {
const latest_Products = [...prev];
const selected_Prod_Id = latest_Products.findIndex((x) => x.id === id);
latest_Products[selected_Prod_Id].inWishList =
!latest_Products[selected_Prod_Id].inWishList; // <-- state mutation
console.log(latest_Products);
return latest_Products;
});
};
The second snippet you not only shallow copy the previous state, but you also shallow copy the element you are updating the inWishList property of.
const AddToWishList = (id) => {
setProducts((currentProdList) => {
const prodIndex = currentProdList.findIndex((p) => p.id === id);
const newFavStatus = !currentProdList[prodIndex].inWishList;
const updatedProducts = [...currentProdList]; // <-- shallow copy state
updatedProducts[prodIndex] = {
...currentProdList[prodIndex], // <-- shallow copy element
inWishList: newFavStatus,
};
console.log(updatedProducts);
return updatedProducts;
});
};
Now the reason these two code snippets function differently is likely due to rendering your app into a StrictMode component.
StrictMode
Specifically in reference to detecting unexpected side effects.
Strict mode can’t automatically detect side effects for you, but it
can help you spot them by making them a little more deterministic.
This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer
When you pass a function to setProducts react actually invokes this twice. This is exposing the mutation in the first example while the second example basically runs the same update twice from the unmutated state, so the result is what you expect.
Example
In my scenario I have a sidebar with filters.. each filter is created by a hook:
const filters = {
customerNoFilter: useFilterForMultiCreatable(),
dateOfOrderFilter: useFilterForDate(),
requestedDevliveryDateFilter: useFilterForDate(),
deliveryCountryFilter: useFilterForCodeStable()
//.... these custom hooks are reused for like 10 more filters
}
Among other things the custom hooks return currently selected values, a reset() and handlers like onChange, onRemove. (So it's not just a simple useState hidden behind the custom hooks, just keep that in mind)
Basically the reset() functions looks like this:
I also implemented a function to clear all filters which is calling the reset() function for each filter:
const clearFilters = () => {
const filterValues = Object.values(filters);
for (const filter of filterValues) {
filter.reset();
}
};
The reset() function is triggering a state update (which is of course async) in each filter to reset all the selected filters.
// setSelected is the setter comming from the return value of a useState statement
const reset = () => setSelected(initialSelected);
Right after the resetting I want to do stuff with the reseted/updated values and NOT with the values before the state update, e.g. calling API with reseted filters:
clearFilters();
callAPI();
In this case the API is called with the old values (before the update in the reset())
So how can i wait for all filters to finish there state updated? Is my code just badly structured? Am i overseeing something?
For single state updates I could simply use useEffect but this would be really cumbersome when waiting for multiple state updates..
Please don't take the example to serious as I face this issue quite often in quite different scenarios..
So I came up with a solution by implementing a custom hook named useStateWithPromise:
import { SetStateAction, useEffect, useRef, useState } from "react";
export const useStateWithPromise = <T>(initialState: T):
[T, (stateAction: SetStateAction<T>) => Promise<T>] => {
const [state, setState] = useState(initialState);
const readyPromiseResolverRef = useRef<((currentState: T) => void) | null>(
null
);
useEffect(() => {
if (readyPromiseResolverRef.current) {
readyPromiseResolverRef.current(state);
readyPromiseResolverRef.current = null;
}
/**
* The ref dependency here is mandatory! Why?
* Because the useEffect would never be called if the new state value
* would be the same as the current one, thus the promise would never be resolved
*/
}, [readyPromiseResolverRef.current, state]);
const handleSetState = (stateAction: SetStateAction<T>) => {
setState(stateAction);
return new Promise(resolve => {
readyPromiseResolverRef.current = resolve;
}) as Promise<T>;
};
return [state, handleSetState];
};
This hook will allow to await state updates:
const [selected, setSelected] = useStateWithPromise<MyFilterType>();
// setSelected will now return a promise
const reset = () => setSelected(undefined);
const clearFilters = () => {
const promises = Object.values(filters).map(
filter => filter.reset()
);
return Promise.all(promises);
};
await clearFilters();
callAPI();
Yey, I can wait on state updates! Unfortunatly that's not all if callAPI() is relying on updated state values ..
const [filtersToApply, setFiltersToApply] = useState(/* ... */);
//...
const callAPI = () => {
// filtersToApply will still contain old state here, although clearFilters() was "awaited"
endpoint.getItems(filtersToApply);
}
This happens because the executed callAPI function after await clearFilters(); is is not rerendered thus it points to old state. But there is a trick which requires an additional useRef to force rerender after filters were cleared:
useEffect(() => {
if (filtersCleared) {
callAPI();
setFiltersCleared(false);
}
// eslint-disable-next-line
}, [filtersCleared]);
//...
const handleClearFiltersClick = async () => {
await orderFiltersContext.clearFilters();
setFiltersCleared(true);
};
This will ensure that callAPI was rerendered before it is executed.
That's it! IMHO a bit messy but it works.
If you want to read a bit more about this topic, feel free to checkout my blog post.
I have an array, which is given as prop into my component named Child. Now, every time a new item is added to the array a fetch against an API should be made.
This array is held in a component named Parent using the useState hook. Whenever I want to add a new item, I have to recreate the array, since I'm not allowed to mutate it directly.
I tried to simplify my use case in the following code snippet:
const Parent = () => {
const [array, setArray] = useState([]);
///...anywhere
setArray(a => [...a, newItem]);
return <Child array={array} />;
}
const Child = ({ array }) => {
useEffect(() => {
array.forEach(element => {
fetch(...);
});
}, [array]);
return ...;
}
Now, my question is: How can I achieve to fetch new data from my API only for the new element but not for the whole array again?
I hope I described my issue good enough. If anything is unclear or missing, let me know.
How about instead fetching the API data in Parent and just passing the end result to Child? That refactoring would provide some benefits:
Parent owns the items array state and knows when and where a new item is added. That makes an incremental fetch very easy. You also get division of container and presentational components for free.
The fetched API data is related to the items array. You probably want to use them together in some way (save api data as state, combine them, etc.). This constellation would promote derived state, which is a more error prone pattern.
Something like following example could already do what you want - add an item via onClick (or somehow else), fetch its data and pass the whole array down to Child:
const Parent = () => {
const [array, setArray] = useState([]);
return (
<div onClick={addItem}>
<Child array={array} />;
</div>
);
function addItem(e) {
const item = createItemSomehow(...)
fetch(...).then(data => setArray([...array, { item, data }]));
}
};
Update:
If you want to keep your structure and API as is, an alternative would be to memoize the previous arrays prop in your Child with a usePrevious hook and look for item changes.
const Child = ({ array }) => {
const prevArray = usePrevious(array);
useEffect(() => {
if (array !== prevArray && array.length) {
//fetch(...)
console.log(`fetch data for index ${array.length - 1}`);
}
});
return (...);
};
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Codesandbox
You could, for example, keep a list of previously fetched items.
const Child = ({ values }) => {
const [fetched, setFetched] = useState([]);
useEffect(() => {
values.forEach(v => {
if (!fetched.includes(v)) {
setFetched(fetched => [...fetched, v]);
fetch(v);
}
});
}, [values, logged]);
https://codesandbox.io/s/affectionate-colden-sfpff
I couldn't find a similar question here, so here it goes:
I created a custom hook useBudget to fetch some data.
const initalState = {
budget_amount: 0,
};
const useBudget = (resource: string, type: string) => {
const [budgetInfo, setBudget] = useState(initalState);
useEffect(
() => {
(async (resource, type) => {
const response = await fetchBudgetInfo(resource, type);
setBudget(response);
})(resource, type);
}, []);
return [budgetInfo];
};
And on the component that uses that hook, I have something like this:
const [budgetInfo] = useBudget(resource, type);
const [budgetForm, setBudgetForm] = useState({ warningMsg: null, errorMsg: null, budget: budgetInfo.budget_amount });
The problem is: The initial state of this component does not update after the fetching. budget renders with 0 initially and keeps that way. If console.log(budgetInfo) right afterwards, the budget is there updated, but the state is not.
I believe that this is happening due to the asynchronicity right? But how to fix this?
Thanks!
I could get to a fix, however, I am not 100% that this is the best/correct approach. As far as I could get it, due to the asynchronicity, I am still reading the old state value, and a way to fix this would be to set the state inside useEffect. I would have:
const [budgetInfo] = useBudget(resource, type);
const [appState, setAppState] = useState({ budget: budgetInfo.budget_amount });
useEffect(
() => {
setAppState({ budget: budgetInfo.budget_amount });
}, [budgetInfo]);
But it's working now!
Working example: https://stackblitz.com/edit/react-wiewan?file=index.js
Effects scheduled with useEffect don’t block the browser from updating the screen - that's why 0 (initialState) is displayed on the screen. After the value is fetched, the component stays the same as there is no change in its own state (budgetForm).
Your solution updates component's state once budgetInfo is fetched hence triggering a re-render, which works but seems to be rather a workaround.
useBudget could be used on its own:
const useBudget = (resource, type) => {
const [budgetInfo, setBudget] = useState(initalState);
const fetchBudgetInfo = async () => {
const response = await (new Promise((resolve) => resolve({ budget_amount: 333 })))
setBudget(response)
}
useEffect(() => {
fetchBudgetInfo(resource, type)
}, [])
return budgetInfo
};
const App = props => {
const { budget_amount } = useBudget('ok', 'ok')
return (
<h1>{budget_amount}</h1>
);
}
fetchBudgetInfo is split out since if we make effect function async (useEffect(async () => { ... })), it will be implicitly returning a promise and this goes against its design - the only return value must be a function which is gonna be used for cleaning up. Docs ref
Alternatively, consider retrieving data without a custom hook:
const fetchBudgetInfo = async (resource, type) => {
const response = await fetch(resource, type)
return response
}
useEffect(() => {
const response = fetchBudgetInfo(resource, type)
setAppState((prevState) => { ...prevState, budget: response.budget_amount });
}, []);
Notably, we are manually merging old state with the new value, which is necessary if appState contains several values - that's because useState doesn't shallowly merge objects (as this.setState would) but instead completely replaces state variable. Doc ref
On a somewhat related note, there is nothing wrong with using object to hold state, however using multiple state variables offers few advantages - more precise naming and the ability to do individual updates.