To be honest, I'm struggling to come up with a way to phrase this question besides "What is happening here?" Take the following React code designed to add incremental items to a list:
import React, { useState } from "react";
import "./styles.css";
let counter = 0;
export default function App() {
const [list, setList] = useState([]);
console.info("Render:", counter, list.join());
return (
<div className="App">
{list.join()}
<button
onClick={() => {
setList((prevList) => {
console.info("Pre-push:", counter, prevList.join());
const newList = [...prevList, "X" + ++counter];
console.info("Post-push:", counter, newList.join());
return newList;
});
}}
>
Push
</button>
</div>
);
}
If you run that code with https://codesandbox.io/s/amazing-sea-6ww68?file=/src/App.js and click the "Push" button four times, I would expect to see "X1" then "X1,X2" then "X1,X2,X3", then "X1,X2,X3,X4". Simple enough right? Instead, it renders "X1" then "X1,X3" then "X1,X3,X5" then "X1,X3,X5,X7".
Now I thought, "huh, perhaps the function that increments counter is being called twice?", so I added the console logging you see, which only mystified me more. In the console, I see:
Render: 0 ""
Pre-push: 0 ""
Post-push: 1 X1
Render: 1 X1
Pre-push: 1 X1
Post-push: 2 X1,X2
Render: 2 X1,X2
Pre-push: 3 X1,X3
Post-push: 4 X1,X3,X4
Render: 4 X1,X3,X4
Pre-push: 5 X1,X3,X5
Post-push: 6 X1,X3,X5,X6
Render: 6 X1,X3,X5,X6
Note that the joined list in the console doesn't match the joined list rendered by React, there is no record of how counter gets bumped from 2 -> 3 and 4 -> 5, and the third item of the list mysteriously changes, despite the fact that I only ever append to the list.
Notably, if I move the ++counter out of the setList delegate, it works as expected:
import React, { useState } from "react";
import "./styles.css";
let counter = 0;
export default function App() {
const [list, setList] = useState([]);
console.info("Render:", counter, list.join());
return (
<div className="App">
{list.join()}
<button
onClick={() => {
++counter;
setList((prevList) => {
console.info("Pre-push:", counter, prevList.join());
const newList = [...prevList, "X" + counter];
console.info("Post-push:", counter, newList.join());
return newList;
});
}}
>
Push
</button>
</div>
);
}
What on earth is going on here? I suspect this is related to the internal implementation of React fibers and useState, but I'm still at a total lost to how counter could be incremented without the console logs right before it and after it showing evidence of such, unless React is actually overwriting console so that it can selectively suppress logs, which seems like madness...
It seems like it's getting invoked twice because it is.
When running in strict mode, React intentionally invokes the following methods twice when running in development mode:
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
Not sure what happens to the console.log calls, but I bet this problem goes away if you switch to production mode.
Related
This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 2 months ago.
I am trying to animate a header on a personal site I'm working on.
It's a simple fade in fade out effect as the user scrolls through the page.
To get this effect what i'm doing is using an Intersection Observer to check wether or not a certain element is in view or not.
When that element is in view, i'd like to change the state elementInView to reflect that or not using a boolean value.
when I console.log the isIntersecting value of the element that the observer is listening to, I can see that it changes True or False based on whether the element is in view or not.
so... since that is working, i'm using that value to set state of elementInView.
BUT!!! the state is not changing when the element is in view, even though it should. I'm not sure what went wrong but I cannot figure it out for the life of me.
here's the code:
import React from 'react';
import { useEffect, useRef, useState } from 'react';
import { Link, useRouteMatch, useNavigate } from 'react-router-dom';
import BoulderImage from "../data/nat_pic_boulders_and_tree.jpeg";
import FenceImage from "../data/nat_pic_fence.jpeg";
import GrassField from '../data/nat_pic_grass.jpeg';
export default function HomePage() {
const navigate = useNavigate();
const firstRef = useRef();
const secondRef = useRef();
const [elementInView, setElementInView] = useState();
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
const entry = entries[0];
setElementInView(entry.isInteresecting);
console.log(elementInView);
})
observer.observe(firstRef.current);
observer.observe(secondRef.current);
}, [elementInView])
return (
<div>
<h1 className="main-intro-header">Welcome Home</h1>
<div className='nav-links'>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</div>
<div className="stop-point-one" ref={firstRef}>stop point</div>
<div className="stop-point-two" ref={secondRef}>stop point</div>
</div>
)}
the list at the end of the useEffect hook. i've tried putting elementInView in there and taking it out and re-running it. no luck.
I think that the thing you are missing is that after calling setElementInView it does not change elementInView on the current render. Only in the next render for that component elementInView will get the new state. And that way you are getting the wrong output to the console.
The way to solve it is to add another useEffect hook with elementInView as a dependency and do you logic there.
You can watch a great video of Jack Herrington regarding this:
https://www.youtube.com/watch?v=RAJD4KpX8LA
Henrys comment links to a better explanation than I can give. Summed up, since setState is performed in an async nature, the value will not be what you set it to immediately after the setState line is ran. If you move your console.log line out of your observer declaration, into the body of the useEffect, it should work as you expect. I put together a little sandbox and spaced some elements apart to put them off screen to show this as well.
https://codesandbox.io/s/observer-test-m5qh88?file=/src/App.js
If you scroll the elements on and off the page with the console open, you'll see it update each time they move off and back onto the screen.
I figured it out!
I saw a simpler way to set the state of these elements as they appear on the screen.
i updated this piece of code:
const [elementInView, setElementInView] = useState();
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
const entry = entries[0];
setElementInView(entry.isInteresecting);
console.log(elementInView);
})
observer.observe(firstRef.current);
observer.observe(secondRef.current);
}, [elementInView])
to this:
import { useInView } from 'react-intersection-observer';
const { ref: firstRef, inView: firstBreakIsVisibile} = useInView();
const { ref: secondRef, inView: secondBreakIsVisibile} = useInView();
I used npm to install the react-intersection-observer dependency.
I'm building this simple quiz app in React where a random Korean hangul from a 40-item Data.json is displayed on the screen and users have 5 buttons with different answers which also come from Data.json.
For clarification, this is my application and the components I'm using. All components are siblings and live inside App.js.
What the application is suppose to do is to create a 5-item random array and select one of these as the right answer before render. If the user taps the right button all the components should be re-rendered with different items. That's why I decided to use the useEffect() hook.
But what is currently happening is:
Every console.log is being displayed twice in the console. The first time variables are empty and in the second time they display the values they should have;
The randomOptions array has 10 items instead of 5;
If the <Question/> component is in the code then I get errors and console.log tells me the values from my useState hooks are undefined. If <Question/> is removed I get the console shown in the picture below.
Console for clarification:
App.js component:
import './App.css';
import Data from './data/data.json';
import Header from './components/Header';
import Question from './components/Question';
import { useState, useEffect } from 'react';
function App() {
const [index, setIndex] = useState(Math.floor(Math.random() * 5)); // Tells us what the index of the right answer is
const [randomOptions, setRandomOptions] = useState([]); // Random array with 5 items from Data.json that are the options
const [question, setQuestion] = useState([]); // The question that goes to <Question/> and people need to guess
const randomizeButtonAnswer = () => {
let previousArray = randomOptions;
setRandomOptions([]);
for (let i=0; i < 5; i++){
let randomNumber = Math.floor(Math.random() * Data.length);
if (!randomOptions.includes(randomNumber) && !previousArray.includes(randomNumber)) {
setRandomOptions(randomOptions.push(randomNumber));
} else {
i--;
}
}
setRandomOptions(randomOptions.sort(() => Math.random() - 0.5));
setQuestion(randomOptions[index]);
}
useEffect(() => {
randomizeButtonAnswer();
}, [index])
console.log('index is ' + index); // 3
console.log('randomOptions is ' + randomOptions); // 33,9,3,26,8,20,0,12,29,25
console.log('question is ' + question); //26
return (
<div className="App">
<Header />
<Question answer={question}/>
</div>
);
}
export default App;
Question.js component
import './Question.css';
import Data from '../data/data.json';
const Question = ({ answer }) => {
return (
<div className="question-wrapper">
<h1 className="question"><span className="highlight">{Data[answer].character}</span></h1>
<span>{Data[answer].character} = {Data[answer].char_id} </span>
</div>
);
}
export default Question;
I'm completely lost here. Not sure why things are being rendered twice or why when present <Question/> makes my variables become undefined. I assume something inside randomizeButtonAnswer is broken but I've been unable to find what exactly.
Small disclaimer: I'm a complete noob to React so if I'm doing something that is wrong or a bad practice do let me know.
Thanks for your time!
I tried to fix your code but it's complicated and wrong at many points so I decided to write the function myself. Sorry about that.
Here is the codesandbox
there are many problem with your code.
About log 2 times. It is <StrictMode />
You should understand more about React State. It not change right in the place you write it, it's queue up and update when your functions end. set 1 state 2 time in 1 function does not mean anything.
Don't do i-- in for loop like that which is complicated to understand how things goes and hard to debug. use while loop or for best just don't. check the condition and log out or have some handle for it.
Wanna log? log inside React Component? use useEffect.
Default state of Question is not [].
Always do conditional render if default value of state is possible be either null or undefined.
Good luck with your React journey.
I'm trying to learn more in-depth the render cycle of function components.
As I'm testing different scenarios of triggering re-renders by experimenting with useEffect and useState, I encounter an example where changing the state continuously(in fact, an intended infinite loop) does not cause the render to be triggered continuously.
Consider the code sample below or on CodeSandbox at: https://co5tj.csb.app/
I was really expecting the render cycle to go into an infinite loop, but it doesn't. It always stops after the 3rd render, as seen in the console log, i
import "./styles.css";
import React, { useEffect, useState } from "react";
function App() {
const [myText, setMyText] = useState("Waiting for text...");
useEffect(() => {
function getMyTextData() {
setTimeout(() => {
setMyText("Hello, here is some text.");
}, 3000);
}
getMyTextData();
}, [myText]);
console.log("triggering render... state = " + myText);
return (
<div>
<h1>{myText}</h1>
</div>
);
}
export default App;
It doesn't lead to an infinite recursion because the second time setMyText is called it doesn't actually result in a state update - because the current state is equal to the next state, so the update is skipped.
Changing it to:
setMyText(myText + "more text");
will make it infinite.
NB: I've asked this on wordpress.stackexchange, but it's not getting any response there, so trying here.
I'm not sure if this is WordPress specific, WordPress's overloaded React specific, or just React, but I'm creating a new block plugin for WordPress, and if I use useState in its edit function, the page is re-rendered, even if I never call the setter function.
import { useState } from '#wordpress/element';
export default function MyEdit( props ) {
const {
attributes: {
anAttribute
},
setAttributes,
} = props;
const [ isValidating, setIsValidating ] = useState( false );
const post_id = wp.data.select("core/editor").getCurrentPostId();
console.log('Post ID is ', post_id);
const MyPlaceholder = () => {
return(
<div>this is a test</div>
);
};
const Component = MyPlaceholder;
return <Component />;
}
If I comment out const [ isValidating, setIsValidating ] = useState( false ); then that console.log happens once. If I leave it in, it happens twice; even if I never check the value of isValidating, never mind calling setIsValidating. I don't want to over-optimize things, but, equally, if I use this block n times on a page, the page is getting rendered 2n times. It's only on the admin side of things, because it's in the edit, so maybe not a big deal, but ... it doesn't seem right. Is this expected behavior for useState? Am I doing something wrong? Do I have to worry about it (from a speed perspective, from a potential race conditions as everything is re-rendered multiple times)?
In your example code, the console.log statement is being immediately evaluated each time and triggering the redraw/re-rendering of your block. Once console.log is removed, only the state changes will trigger re-rendering.
As the Gutenberg Editor is based on Redux, if the state changes, any components that rely on that state are re-rendered. When a block is selected in the Editor, the selected block is rendered synchronously while all other blocks in the Editor are rendered asynchronously. The WordPress Gutenberg developers are aware of re-rendering being a performance concern and have taken steps to reduce re-rendering.
When requesting data from wp.data, useEffect() should be used to safely await asynchronous data:
import { useState, useEffect } from '#wordpress/element';
export default function MyEdit(props) {
...
const [curPostId, setCurPostId] = useState(false);
useEffect(() => {
async function getMyPostId() {
const post_id = await wp.data.select("core/editor").getCurrentPostId();
setCurPostId(post_id);
}
getMyPostId();
}, []); // Run once
const MyPlaceholder = () => {
return (
<div>Current Post Id: {curPostId}</div>
);
};
const Component = MyPlaceholder;
return <Component />;
}
As mentioned in the question, useState() is used in core blocks for setting and updating state. The state hook was introducted in React 16.8, its a fairly recent change and you may come across older Gutenberg code example that set state via the class constructor and don't use hooks.
Yes, you have to worry about always put an array of dependencies, so that, it won't re-render, As per your query, let's say are planning to edit a field here is the sample code
const [edit, setEdit]= useState(props);
useEffect(() => {
// logic here
},[edit])
that [edit] will check if there is any changes , and according to that it will update the DOM, if you don't put any [](array of dependencies) it will always go an infinite loop,
I guess this is expected behavior. If I add a similar console.log to native core blocks that use useState, I get the same effect. It seems that WordPress operates with use strict, and according to this answer, React double-invokes a number of things when in strict mode.
I'm a newbie on frontend development and learning React. Now I'm trying to build a hello-world project.
After executing npx create-react-app myapp, I got an initial React project and I just coded in the file App.js.
import React, {useState} from 'react';
var counter = 0;
function App() {
const [counter2, setCount] = useState(0);
const increment = () => {
setCount(counter2 + 1);
};
return(
<div>
<button onClick= {increment}>Increment</button>
<h1>{counter++}</h1> // 1, 3, 5, 7... WHY???
<h1>{counter2}</h1> // 0, 1, 2, 3...
</div>
);
}
export default App;
After executing npm start, I got my index page, which contains three parts: a button and two numbers.
To my surprise, when I click the button, counter2 is increased as expected, but counter is increased twice. Meaning that keeping clicking the button gives me the result as below:
1 0, 3 1, 5 2...
Why is the global variable counter increased two by two, instead of one by one?
Furthermore, what is the difference between React State and common global variable?
When you wrap a component in <React.StrictMode>, it will run certain functions twice, one of these being the function body of your functional component:
This is done by intentionally double-invoking the following functions:
... Function component bodies
- React docs
This is only done in dev mode, and the idea behind doing it is to help you catch side-effects within your project.
It may seem as though your component is only being executed once though, as putting a console.log() inside of your functional component will only run once per state change. This is because, as of React 17, they have updated the console.log method to not log on the second invocation of your function:
Starting with React 17, React automatically modifies the console
methods like console.log() to silence the logs in the second call to
lifecycle functions
- React docs
However, there is a workaround to this by saving a reference to the console.log method, and using that to perform your logs. Doing this will allow you to see that your component is being executed twice:
const log = console.log;
function App() {
const [counter2, setCount] = useState(0);
const increment = () => {
setCount(counter2 + 1);
};
log("Rendering, counter is:", counter);
return(
<div>
<button onClick= {increment}>Increment</button>
<h1>{counter++}</h1>
<h1>{counter2}</h1>
</div>
);
}
The above will output the following when the component mounts, showing that the function body is running twice:
Rendering, counter is: 0
Rendering, counter is: 1
If you remove the <React.StrictMode> component then counter will increase by one each render, as React will no longer double-invoke your functional component body, and your component body will only be called once:
ReactDOM.render(<App />, document.getElementById('root'));
In terms of global variables vs state, the main difference has been pointed out in a comment above. That is that when you update your state with setMethodName(), you'll cause your component body to rerender, which doesn't happen when you update a normal variable as React won't be aware of the changes made to it.