React - Mutating the state argument from setSate - reactjs

I know we are not supposed to mutate a state object directly. Does this apply to the state object available in the call back of setState. I am looking to avoid using the spread operator too much. For example, can we do the following
setState(prevState => {
prevState.count += 1;
prevState.data.list[0].item.prop = 34;
return prevState;
});

you should always make copies from objects you modify to update state, otherwise you will face bugs in your application since you have the same reference. Each part of state you modify you should make a copy, which in really nested states could be difficult.
One thing advisable is to flat your state if you can. This way you avoid really nested states, which would lead to multiple copies and prone to error. Your example, it looks you could have 2 different states count and items:
const [count, setCount] = useState(0);
const [items, setItems] = useState(dataItems);
// some update state function logic call
setCount(count => ++count);
setItems(items => {
const nextItems = [...items]; // new arrray copy
nextItems[0] = { ...nextItems[0], prop: 34}; // new copy obj
return nextItems;
});
otherwise your state update would look like:
setState(prevState => {
const nextState = { ...prevState }
nextState.count += 1;
nextState.data = { ...nextState.data }
nextState.data.list = [ ...nextData.data.list ]
const item = nextState.data.list[0]
nextState.data.list[0].item = { ...nextState.data.list[0].item , prop: 34 };
return nextState;
});

Related

React useEffect: Prevent crashing of app when updating state that is required in the dependency array

I am in a situation where I need to update a certain state in the useEffect, while depending on the value of the state itself, and had to put it in the dependency array - due to a warning from eslint-react-hooks. However, due to the fact that I am updating a state that is present in the dependency array, this causes the app to crash due to what I believe is an infinite loop happening in the useEffect.
The example below shows a simple example:
const [list, setList] = useState(list);
useEffect(() => {
const currentList = [...list];
const newList = currentList.push(newItem);
setList(newList);
}, [newItem, list]);
I have came up with a solution to use the previous state argument in the set state method of the useState hook to update accordingly. This effectively removes the need for the dependency in the dependency array.
const [list, setList] = useState(list);
useEffect(() => {
setList((prevList) => {
const currentList = [...prevList];
const newList = currentList.push(newItem);
return newList;
});
}, [newItem]);
I would like to ask if anyone have faced this issue before, and have found a better solution? Thank you!
The general approach of using the setter's callback instead of having to refer to the outer state value is perfectly normal and acceptable. You can slim it down too:
useEffect(() => {
setList(prevList => [...prevList, newItem]);
}, [newItem]);
But it's a little bit strange to have a separate newItem state that immediately gets merged into the list. Consider if you could merge them together so you only have one stateful variable in the end. Instead of doing
setNewItem(theNewItem);
do
setList(prevList => [...prevList, theNewItem]);
and then, wherever else you refer to newItem, change it to list[list.length - 1] - or do
const newItem = list[list.length - 1];
in the body of the component.
That approach won't work for all situations, but consider whether it'd be applicable to yours.
You can keep the list unchanged and use another state to update the ui:
const [list, setList] = useState(list);
const [count, setCount] = useState(list.length);
useEffect(() => {
list.push(newItem);
setList(list);
setCount(list.length);
}, [newItem, list]);

Having to dynamically change the state : how to avoid an infinite-loop?

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>
);
}

React hooks: useEffect not being triggered on array of objects

For readability im going to strip out a lot of functionality in my examples. However, essentially I have a useEffect (shown below) that has a dependency that tracks the state.cards array of card objects. My assumption was that if that state.cards property changes then the useEffect should trigger. However, that's not exactly proving to be the case.
Below are two solutions that are being used. I want to use the first one since it's in constant time. The second, while fine, is linear. What confuses me is why the second option triggers the dependency while the first does not. Both are return a clone of correctly modified state.
This does not trigger the useEffect dependency state.cards.
const newArr = { ...state };
const matchingCard = newArr.cards[action.payload]; <-- payload = number
matchingCard.correct += 1;
matchingCard.lastPass = true;
return newArr;
This does trigger the useEffect dependency state.cards.
const newArr = { ...state };
const cards = newArr.cards.map((card) => {
if (card.id === action.payload.id) {
card.correct += 1;
card.lastPass = true;
}
return card;
});
return { ...newArr, cards };
useEffect
useEffect(() => {
const passedCards = state.cards.filter((card) => {
return card.lastPass;
});
setLearnedCards(passedCards);
const calculatePercent = () => {
return (learnedCards.length / state.cards.length) * 100;
};
dispatch({ type: 'SET_PERCENT_COMPLETE', payload: calculatePercent() });
}, [learnedCards.length, state.cards]);
State
const initialState = {
cards: [], <-- each card will be an object
percentComplete: 0,
lessonComplete: false,
};
Solution: Working solution using the first example:
const newCardsArray = [...state.cards];
const matchingCard = newCardsArray[action.payload];
matchingCard.correct += 1;
matchingCard.lastPass = true;
return { ...state, cards: newCardsArray };
Why: Spreading the array state.cards creates a new shallow copy of that array. Then I can make modifications on that cloned array and return it as the new value assigned to state.cards. The spread array has a new reference and that is detected by useEffect.
My best guess is that in the second working example .map returns a new array with a new reference. In the first example you are just mutating the contents of the array but not the reference to that array.
I am not exactly sure how useEffect compares, but if I remember correctly for an object it is just all about the reference to that object. Which sometimes makes it difficult to use useEffect on objects. It might be the same with arrays too.
Why dont you try out:
const newCardsArray = [...state.cards]
// do your mutations here
should copy the array with a new ref like you did with the object.

React / Redux - Getting variable to update properly on change of global state

I currently have a component (A) that has many local state variables, and also uses useSelector((state) => state.app.<var>. Some of the local state variables rely on that global state, and I need to render one local variable onto the screen.
code example:
const ComponentA = () => {
const globalState = useSelector((state) => state.app.globalState);
// CASE 1: WORKS
const localState1 = 'hello' + globalState + 'world';
// CASE 2: DOESN't WORK
const [localState1, setLocalState1] = useState(null);
const [lcoalState2, setLocalState2] = useState(null);
useEffect(() => {
}, [localState1]);
useEffect(() => {
setLocalState1('hello' + globalState + 'world')
}, [localState2]);
return (
.... code changes
<p>{localState1}</p>
);
}
Case 1 results in the localState1 properly being updated and rendered on the screen, but in Case 2 localState1 is not updated on the screen.
I have no idea why setting localState1 to a regular variable instead of a local state variable works. I thought that a change in local state would cause a re-render on the DOM, meaning I could visually see the change. Could someone explain why the local state case fails to update and how to fix it?
You need to make your useEffect aware of globalState change by adding it to dependencies array (anyway you should get a linting warning when you forget it, like in your case):
const ComponentA = () => {
const globalState = useSelector((state) => state.app.globalState);
const [localState1, setLocalState1] = useState(null);
useEffect(() => {
setLocalState1("hello" + globalState + "world");
}, [globalState]);
return <p>{localState1}</p>;
};
Moreover, you don't really need a state for it, just implement the selector according to your needs, it always will update on state.app.globalState change:
const ComponentA = () => {
const globalStateString = useSelector(
(state) => `hello ${state.app.globalState} world`
);
return <p>{globalStateString}</p>;
};

Fetching data in useEffect with an array as dependency should only be called on new elements

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

Resources