I'm using React.js with hooks and trying to change a nested state value.
Since I don't want to mutate the state directly, I'm using lodash's "CloneDeep" function.
The state changes successfully, but a re-render doesn't happen so I don't see the change in the page until I re-enter it.
More info (checks results in my web console):
console.log(logicalLinesDeepCopy == logicalLines); => false
console.log(logicalLinesDeepCopy === logicalLines); => false
console.log(logicalLines[menuLineId].content[menuSentenceId].triggers); => []
console.log(logicalLinesDeepCopy[menuLineId].content[menuSentenceId].triggers); => [(object)]
My code:
// relevant imports:
import CloneDeep from "lodash/cloneDeep";
// relevant state initiation: (the value is filled before I call the relevant function)
const [logicalLines, setLogicalLines] = useState(null);
// relevant function:
const addTriggerToListInState = (trigger) => {
// Deep copy:
let logicalLinesDeepCopy = CloneDeep(logicalLines);
// Change the deep copy:
const lineCopy = logicalLinesDeepCopy[menuLineId];
const triggersListCopy = lineCopy.content[menuSentenceId].triggers;
triggersListCopy.push(trigger);
// Some debug checks:
console.log(logicalLinesDeepCopy == logicalLines);
console.log(logicalLinesDeepCopy === logicalLines);
console.log(logicalLines[menuLineId].content[menuSentenceId].triggers);
console.log(logicalLinesDeepCopy[menuLineId].content[menuSentenceId].triggers);
// Setting the state:
setLogicalLines(logicalLinesDeepCopy);
};
So it looks like both the reference and the nested content of the new state are different from the previous one, then why doesn't it cause a re-render and how can I change it to cause it?
Thanks!
Sapir
Related
I am working on a history component in my React project. I have the history state and a setter function setHistory() . I have two functions for handling clicks: handleClick() and removeHistoryItem(), which are called with different buttons. In both of them, I am changing the state with the setHistory() function, but it doesn't work in the handleClick() one.
const removeHistoryItem = () => {
const newHistory = history.filter(item => item.id != id)
setHistory(newHistory)
}
const handleClick = () => {
let newHistory = history
let [selectedGame] = history.filter(item => item.id == id)
const index = history.indexOf(selectedGame)
selectedGame.isFavorite = !selectedGame.isFavorite
newHistory.splice(index, 1, selectedGame)
localStorage.setItem('history', JSON.stringify(newHistory))
setHistory(newHistory)
}
The functions are next to each other so there shouldn't be a problem with the scope. Both functions are executing the other operations well (tested with console.log(), the localStorage is changed just fine, the splice and filter functions are working also). Even the value I am passing to the setHistory() function is correct, just the state is not changed. It's like the setHistory() function is not even called. I am not getting any errors, what could the problem be?
Error is due to Reference equality, both newHistory and history in your handleClick function are referencing same array in "memory", which you mutate with splice, but it is still the same object, react will not detect the change and will not fire the rerender, just because oldValue (history) === newValue (newHistory).
So you need to "recreate" the array in this case. For removeHistoryItem all is ok due to .filter returns the new array already, so new reference, new object.
setHistory([...newHistory]);
I have component which receives data from other file and setting into state:
const [sortedPlans, setSortedPlans] = useState(
data.Plans.sort((a, b) => b.IndustryGrade - a.IndustryGrade)
);//data is from external file
After setting the state, sorting the data and rendering the initial screen, I have function that sorts the sortedPlans whenever it is called:
const sort = (event) => {
console.log(event);
const newArr = sortedPlans.sort((a, b) => {
return b[event] - a[event];
});
console.log(newArr);
return setSortedPlans(newArr);
};
The problem is that this is never triggering a re-render of the component. I want when I set the new state to be able to see it inside the jsx. Why when I console.log(newArr) the results are correctly sorted but this setting of the state not triggering re-render? Here is the sandbox:
https://codesandbox.io/s/charming-shape-r9ps3p?file=/src/App.js
Here you go: Codesandbox demo.
You should first make a shallow copy of the array you want to modify. Then set that array as the new state. Then it re-renders the component and you are able to filter like you want.
const sort = (event) => {
console.log(event);
//shallow copying the state.
const newArr = [...sortedPlans];
//modifying the copy
newArr.sort((a, b) => {
return b[event] - a[event];
});
console.log(newArr); //results here are correctly sorted as per event
//setting the state to the copy triggers the re-render.
return setSortedPlans(newArr);
};
I am using an array of components that are interested depending on various conditions i.e the order and number of elements in the array are dynamic as shown below:
useEffect(() => {
const comp = [];
// if(condition1===true){
comp.push(<MyComp onChange={onValueChange} />);
// }
// if(condition2===true){
comp.push(<YourComp onChange={onValueChange} />);
// }
// if(condition3===true){
comp.push(<HisComp onChange={onValueChange} />);
// }
setComponents(comp);
}, []);
To each of the components in the array, there could be some sort of input control like input-text, input-number, text-area, chips, radio, checkbox, etc.
So there is an onChange event linked to each of these components.
I am using a common onValueChange function which is passed as a callback to these components. In the onValueChange I need 2 things:
changed value (from child component)
activeIndex (from same component)
const onValueChange = (val) => {
console.log("onChange Valled", val, "activeIndex==", activeIndex);
};
But here I am not able to fetch the updated value on activeIndex, it always gives zero no matter in what active step I am in.
Sandbox DEMO
useEffect(() => {
setComponents((previousValues)=>{
// if you want to add on previous state
const comp = [...previousValues];
// if you want to overwrite previous state
const comp = [];
if(condition1===true){
comp.push();
}
if(condition2===true){
comp.push();
}
if(condition3===true){
comp.push();
}
return comp;
});
}, []);
Try using useCallback with dependency array. Also try to avoid storing components in state - the office advice - what shouldn’t go in state?
const onValueChange = useCallback((val) => {
console.log("onChange Valled", val, "activeIndex==", activeIndex);
},[activeIndex];
For rendering try something like below.
condition1===true && <MyComp onChange={onValueChange} />
or create a function which returns the component eg: getComponent(condition) and use this in render/return. Make sure you wrap getComponent in useCallback with empty dependency array []
I am using useEffect in react to listen to redux(easy-peasy) state change, but I want to listen to 1st value change only.
Because when my page loads the state has a default value and then API call is made and hence data changes but the API is a polling API, hence it keeps getting the data again and again in a short interval of time. But one of my requirement is to listen only to the 1st API data.
This is what I tried:
1st Approach with empty dependency
useEffect(() => {
// my code
},[])
In this case, I get the default value of my state and not the 1st API response.
2nd Approach with state property in the dependency
useEffect(() => {
// my code
},[myState])
In this case, I keep getting the updated response from the API
both of these approaches didn't work for me. So please suggest a better solution.
You can do so using a ref variable and comparing the state with initial state (which could be null, undefined, empty object depending on your implementation):
const hasRun = useRef(false)
useEffect(() => {
if (!hasRun.current && myState !== initialState) {
hasRun.current = true
// run my code
}
},[myState])
A ref variable won't participate in re-rendering.
What I usually do with this is to have a "previous" state.
You can use this hook for to do that one:
const usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]); // only re-run if value changes
// return previous value (happens before update in useEffect)
return ref.current;
}
You can then do:
// I usually create a distinct initial state
const [var, setVar] = useState(null);
const prevVar = usePrevious(var);
useEffect(() => {
if (var !== prevVar && var !== null) {
// logic here
}
}, [prevVar, var]);
Yers you can simplify this by not using the usePrevious hook, but the hook is quite handy and can be used to check the previous value and the current one.
I have a functional component that keeps a state. This state I try to manipulate using an onClick event in an SVG. The SVG is in another component and has the addAndRemoveSelectedCabin method passed to it via props. I loop through the elements in an useEffect and add an eventListener. This doesn't work. The useEffect with the selectedCabins dependency logs the new number only. It seems the state returns to the initial state after every stateChange.
This is the state and method in the parent component.
const [selectedCabins, setSelectedCabins] = useState([0]);
const addRemoveSelectedCabin = id => {
const newArr = [...selectedCabins, id];
setSelectedCabins(newArr);
}
useEffect(() => {
console.log(selectedCabins);
}, [selectedCabins])
This is how I call the method. [UPDATE]
useEffect(() =>
{
const cabins = document.querySelectorAll(".cabin");
cabins.forEach(cabin =>
{
const id = cabin.getAttributeNS(null, "id").substring(1, 5);
const found = cabinsData.find(el => el.id === id)
if (found && found.status === "available")
{
cabin.classList.add("green")
cabin.addEventListener('click', () => addRemoveSelectedCabin(id));
} else if (found && found.status === "booked")
{
cabin.classList.add("gray")
}
})
}, [])
Console:
[0]
(2) [0, "1105"]
(2) [0, "1101"]
This works if I put the onClick directly in the SVG element. Does anyone know why this is?
<rect
id="C1105"
x="749.4"
y="58.3"
className="cabin"
width="36.4"
height="19.9"
onClick={() => addRemoveSelectedCabin(1105)}
>
<title>1105</title>
</rect>
As I said in my comment, you are binding addRemoveSelectedCabin in the first render. useEffect is only executed once since you pass an empty dependency list. addRemoveSelectedCabin closes over selectedCabins which at that point in time has the value [0].
Why am I seeing stale props or state inside my function? from the React documentation has more information about this.
The solution in your case is simple: Pass a function to the setter to get the "current" state value. Don't reference the state value in the component:
const addRemoveSelectedCabin = id => {
setSelectedCabins(selectedCabins => [...selectedCabins, id]);
}
Having said that, this is still an odd thing to do in React world. You should reevaluate your assumptions that make you think you have to do it that way.
It's not all the elements that should have a click listener.
Depending on how you actually render the elements, that's easy to do. JSX/React is just JavaScript. Whether you have a condition that adds the event handler or not or whether you have a condition that sets onClick or not is basically the same.
But without a more complete example there is not much we can suggest.