setState accept function vs object - reactjs

Lets say I have a state that I would like to update:
state = {
description: "",
notes: ""
}
Is there a difference if I update the state with just an object, if I do not need to use prevState?
this.setState({description: "Blue"});
vs
this.setState((prevState)=> ({description: "Blue"}));

I've created the demo to visualize the difference
/* When we doing this state gets update after the for loops runs so,
this.state.count is same for each repeat so even if we expecting to
count increase by 5 its actually increment by 1 */
updateCountByFive = () => {
for (let a = 0; a < 5; a++) {
this.setState({
count: this.state.count + 1
});
}
};
/* In this case by using callback to set the new state,
react is smart enough to pass the latest count value to the callback
event if that new change not rendered */
updateCountByFiveCallback = () => {
for (let a = 0; a < 5; a++) {
this.setState(prevState => {
return {
count: prevState.count + 1
};
});
}
};
So it's a good practice to use callback version when you need to use current state to set the next state, since it will prevent some issues as above
This is what react doc says.
React may batch multiple setState() calls into a single update for performance.
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.
there is a nice article here

Related

How to change more than 1 state in React-easy-state?

I am not able to mutate more than 1 state through one function using React-easy-state
I have used batch in my following example and also used mutation separately. However, the code somehow disregards my 2nd state.
piece of the code looks like this :
batch(() => {
// batch is used to test out whether it will trigger both store changes.
store.counter = store.counter + 1;
store.sumNum = 10; // this is never updated/mutated.
});
for reproduction link to sandbox.
You don't have any state called sumNum.
// your store
const myStore = store({
rndNum: "",
counter: 1,
someNum: 2, <--- not sumNum
showRes: false
});
Change it to someNum and it works.

React useEffect hook with empty dependency render multiple times

I always thought useEffect with empty dependencies will only render once. Here is my code snippet but it renders 2 times:
useEffect(() => {
let entryDataArray = [];
for (let i = 0; i < days; ++i) {
let entryData = [];
let entries = eventEntryList[i];
console.log('entries = ', entries);
for (var j = 0; j < entries.length; ++j) {
console.log('j = ', j);
console.log('entries[j] 1111111 = ', entries[j]);
if (entries[j].runGroup[i] === NOT_ATTENDING) {
continue;
}
console.log('entries[j] 2222222 = ', entries[j]);
let entry = {
lastName: entries[j].userLastName,
firstName: entries[j].userFirstName,
email: entries[j].email,
carNumber: entries[j].carNumber,
paymentMethod: entries[j].paymentMethod,
entryFee: entries[j].entryFee,
paymentStatus: entries[j].paymentStatus ? 'Paid' : 'Unpaid'
};
entryData.push(entry);
}
entryDataArray.push(entryData);
}
setEntryListArray(entryDataArray);
setShowLoading(false);
}, []);
console output shows it renders 2 times. The first time, for loop works as it supposed to - "continue" works under "if (entries[j].runGroup[i] === NOT_ATTENDING)". The 2nd time, "continue" did not get executed.
Am I missing something?
To be clear: useEffect with an empty dependency array runs every time the component mounts. This could be more than once if you have unmounted and remounted your component by accident.
First make sure you're not in StrictMode, as this would be expected in strict mode.
If you aren't in strict mode include a return function in your useEffect which will run on every unmount to detect whether or not your component is unmounting.
useEffect(() => {
// your code
return () => {
// will run on every unmount.
console.log("component is unmounting");
}
}, [])
I thought you run your app in dev mode right?. On Dev mode every time you make the change it hot reload your component every time you make the change to your code.

React hook rendering an extra time

My code is causing an unexpected amount of re-renders.
function App() {
const [isOn, setIsOn] = useState(false)
const [timer, setTimer] = useState(0)
console.log('re-rendered', timer)
useEffect(() => {
let interval
if (isOn) {
interval = setInterval(() => setTimer(timer + 1), 1000)
}
return () => clearInterval(interval)
}, [isOn])
return (
<div>
{timer}
{!isOn && (
<button type="button" onClick={() => setIsOn(true)}>
Start
</button>
)}
{isOn && (
<button type="button" onClick={() => setIsOn(false)}>
Stop
</button>
)}
</div>
);
}
Note the console.log on line 4. What I expected is the following to be logged out:
re-rendered 0
re-rendered 0
re-rendered 1
The first log is for the initial render. The second log is for the re-render when the "isOn" state changes via the button click. The third log is when setInterval calls setTimer so it's re-rendered again. Here is what I actually get:
re-rendered 0
re-rendered 0
re-rendered 1
re-rendered 1
I can't figure out why there is a fourth log. Here's a link to a REPL of it:
https://codesandbox.io/s/kx393n58r7
***Just to clarify, I know the solution is to use setTimer(timer => timer + 1), but I would like to know why the code above causes a fourth render.
The function with the bulk of what happens when you call the setter returned by useState is dispatchAction within ReactFiberHooks.js (currently starting at line 1009).
The block of code that checks to see if the state has changed (and potentially skips the re-render if it has not changed) is currently surrounded by the following condition:
if (
fiber.expirationTime === NoWork &&
(alternate === null || alternate.expirationTime === NoWork)
) {
My assumption on seeing this was that this condition evaluated to false after the second setTimer call. To verify this, I copied the development CDN React files and added some console logs to the dispatchAction function:
function dispatchAction(fiber, queue, action) {
!(numberOfReRenders < RE_RENDER_LIMIT) ? invariant(false, 'Too many re-renders. React limits the number of renders to prevent an infinite loop.') : void 0;
{
!(arguments.length <= 3) ? warning$1(false, "State updates from the useState() and useReducer() Hooks don't support the " + 'second callback argument. To execute a side effect after ' + 'rendering, declare it in the component body with useEffect().') : void 0;
}
console.log("dispatchAction1");
var alternate = fiber.alternate;
if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
// This is a render phase update. Stash it in a lazily-created map of
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
didScheduleRenderPhaseUpdate = true;
var update = {
expirationTime: renderExpirationTime,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
if (renderPhaseUpdates === null) {
renderPhaseUpdates = new Map();
}
var firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate === undefined) {
renderPhaseUpdates.set(queue, update);
} else {
// Append the update to the end of the list.
var lastRenderPhaseUpdate = firstRenderPhaseUpdate;
while (lastRenderPhaseUpdate.next !== null) {
lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
}
lastRenderPhaseUpdate.next = update;
}
} else {
flushPassiveEffects();
console.log("dispatchAction2");
var currentTime = requestCurrentTime();
var _expirationTime = computeExpirationForFiber(currentTime, fiber);
var _update2 = {
expirationTime: _expirationTime,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
// Append the update to the end of the list.
var _last = queue.last;
if (_last === null) {
// This is the first update. Create a circular list.
_update2.next = _update2;
} else {
var first = _last.next;
if (first !== null) {
// Still circular.
_update2.next = first;
}
_last.next = _update2;
}
queue.last = _update2;
console.log("expiration: " + fiber.expirationTime);
if (alternate) {
console.log("alternate expiration: " + alternate.expirationTime);
}
if (fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)) {
console.log("dispatchAction3");
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
var _eagerReducer = queue.eagerReducer;
if (_eagerReducer !== null) {
var prevDispatcher = void 0;
{
prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
}
try {
var currentState = queue.eagerState;
var _eagerState = _eagerReducer(currentState, action);
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
_update2.eagerReducer = _eagerReducer;
_update2.eagerState = _eagerState;
if (is(_eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
{
ReactCurrentDispatcher$1.current = prevDispatcher;
}
}
}
}
{
if (shouldWarnForUnbatchedSetState === true) {
warnIfNotCurrentlyBatchingInDev(fiber);
}
}
scheduleWork(fiber, _expirationTime);
}
}
and here's the console output with some additional comments for clarity:
re-rendered 0 // initial render
dispatchAction1 // setIsOn
dispatchAction2
expiration: 0
dispatchAction3
re-rendered 0
dispatchAction1 // first call to setTimer
dispatchAction2
expiration: 1073741823
alternate expiration: 0
re-rendered 1
dispatchAction1 // second call to setTimer
dispatchAction2
expiration: 0
alternate expiration: 1073741823
re-rendered 1
dispatchAction1 // third and subsequent calls to setTimer all look like this
dispatchAction2
expiration: 0
alternate expiration: 0
dispatchAction3
NoWork has a value of zero. You can see that the first log of fiber.expirationTime after setTimer has a non-zero value. In the logs from the second setTimer call, that fiber.expirationTime has been moved to alternate.expirationTime still preventing the state comparison so re-render will be unconditional. After that, both the fiber and alternate expiration times are 0 (NoWork) and then it does the state comparison and avoids a re-render.
This description of the React Fiber Architecture is a good starting point for trying to understand the purpose of expirationTime.
The most relevant portions of the source code for understanding it are:
ReactFiberExpirationTime.js
ReactFiberScheduler.js
I believe the expiration times are mainly relevant for concurrent mode which is not yet enabled by default. The expiration time indicates the point in time after which React will force a commit of the work at the earliest opportunity. Prior to that point in time, React may choose to batch updates. Some updates (such as from user interactions) have a very short (high priority) expiration, and other updates (such as from async code after a fetch completes) have a longer (low priority) expiration. The updates triggered by setTimer from within the setInterval callback would fall in the low priority category and could potentially be batched (if concurrent mode were enabled). Since there is the possibility of that work having been batched or potentially discarded, React queues a re-render unconditionally (even when the state is unchanged since the previous update) if the previous update had an expirationTime.
You can see my answer here to learn a bit more about finding your way through the React code to get to this dispatchAction function.
For others who want to do some digging of their own, here's a CodeSandbox with my modified version of React:
The react files are modified copies of these files:
https://unpkg.com/react#16/umd/react.development.js
https://unpkg.com/react-dom#16/umd/react-dom.development.js

Updating arrays in react component state

In the function below, I am trying to update the state of a react component; the animalMix item is an array. I take a copy, update it and then try to overwrite the original. I have checked that the new array (newAnimalsHeld) is updated correctly, but this is not reflected when i set animalMix in state equal to it.
The whole thing can be seen in context here:
https://codepen.io/timsig/pen/XVdbdo?editors=0010
Many thanks for any help.
removePair(){
console.log('Match!');
console.log(this.flipped);
let animalsHeld = [...this.state.animalMix];
let theMatch = this.flipped[0].props.animal;
let newAnimalsHeld = animalsHeld.map(function(animal){
if (animal.animal === theMatch) {
console.log('MATCH! Animal: ' + animal.animal + 'Match:' + theMatch);
return {};
}else{
return animal;
}
});
console.log('New Animals held: ', newAnimalsHeld);
this.setState({
animalMix: newAnimalsHeld,
doTurn: true
});
this.flipped = [];
console.log(this.state.doTurn, this.state.animalMix);
}
setState is an asynchronous function. However, you can print to console after state has updated in the following manner:
this.setState({
animalMix: newAnimalsHeld,
doTurn: true
},() => {console.log(this.state.doTurn, this.state.animalMix);});

Why is concat on array inside this.state not working?

I'm trying to add rendered dates inside of my array located in my component state under the key date, but it always returns 0.
constructor(props) {
super(props);
this.state = {
dates: []
}
}
componentWillMount() {
this.renderDateComp()
}
renderDateComp() {
for(var i = 0; i < 5; i++) {
var dat = new Date(Date().valueOf());
dat.setDate(dat.getDate() + i);
this.setState({ dates: this.state.dates.concat(dat) });
console.log(this.state.dates); //prints length of 0
}
}
Well firstly, state transitions aren't immediate, they are asynchronous (in most circumstances). Think of it like every time you set the state, it adds that state change to a queue of changes and React is continuously working through that queue, doing all those state changes. So if you change the state, then on the next line of code immediately print the state, the state probably won't have changed because the console.log happens IMMEDIATELY after the state change is added to the list, but BEFORE the change has actually gone through that queue.
Secondly, it's probably better to set the state AFTER the for loop, so you only have to set it once, not 5 times.
The recommended way to do it would be to:
Call setState only once after you have the final dates array.
Log this.state.dates in the callback of setState (it will only be called after this.state has been properly updated by React.
The key point to note here is that setState is asynchronous. You probably do not need to console.log the array in your real code.
Read more about setState here.
constructor(props) {
super(props);
this.state = {
dates: []
}
}
componentWillMount() {
this.renderDateComp()
}
renderDateComp() {
var dates = this.state.dates;
for (var i = 0; i < 5; i++) {
var dat = new Date(Date().valueOf());
dat.setDate(dat.getDate() + i);
dates = dates.concat(dat);
}
this.setState({
dates
}, () => {
console.log(this.state.dates); //prints length of 5
});
}
It's worth reading the documentation of setState(). https://facebook.github.io/react/docs/react-component.html#setstate
It will queue the change, but there is a callback version if you want to use the new value somehow.
this.setState((prevState, props) => {
return {myInteger: prevState.myInteger + props.step};
});
More specifically, if you do this:
this.setState({ dates: this.state.dates.concat(dat) }, () => console.log(this.state.dates));
You'll get the result you're expecting.

Resources