I test some code and make me confused
There are two reproduction,
use setState function set State to same previous state value
const [state, setState] = useState(1)
setState(() => 1)
use setState function set State to same value(always same ex: 1, true, something else), but different initial state
const [state, setState] = useState(1)
setState(() => 2)
This is reproduction sample 1
always set state to same value
open codesandbox console first and clear it first.
what happened
click, state is already 1, setState function set state to 1,
component function does not re-run
// console show
test
test
test
This is reproduction sample 2
set state to certain value
I always set state to 2.
First click, state is change from 1 to 2, so component function re-run
// console
test
app
Second click, state already is 2, but why component function still re-run
// console
test
app <-------------- here component function re-run
Third click, state already is 2 , component function does not re-run
// console
test
The Problem is Here
In Sample 2, second click button, state is same as
previous, but is still re-run component.
And we go back to see Sample 1 and third click in Sample 2, these two state change step is same as Sample 2 second click, they are all same set same state compare to previous state, but how they output is different
As I know, state change will cause component function re-run.
What I expect is Sample 2 second click, component function is not re-run
Let's focus only on Simple 2 as Simple 1 flow is what a developer would expect.
Simple 2 logs are as follow:
1. app // first render
2. test // button click, log from the callback
3. app // state change from '1' to '2', second render
4. test // second button click
5. app // no state change, why the log?
6. test // 3rd button click
7. test // 4th button click etc.
So the main question is why there is a 5th log of 'app'.
The reasoning behind it hides somewhere in React docs under Bailing out of a state update:
Note that React may still need to render that specific component again
before bailing out. That shouldn’t be a concern because React won’t
unnecessarily go “deeper” into the tree.
In simple words, its just an edge case where React needs another render cycle but it doesn't goes into the Nodes tree (i.e it doesn't run the return statement, in your example it's a React.Fragment <></>)
A more concrete example, notice the addition log of "A":
const App = () => {
const [state, setState] = React.useState(0);
useEffect(() => {
console.log("B");
});
console.log("A");
return (
<>
<h1>{state}</h1>
<button onClick={() => setState(42)}>Click</button>
</>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
Related
In the code below, whenever I get new props from the parent, the new props are logged correctly on the console, but the rendered HTML is never updated after the initial render:
export default function(props) {
const [state, setState] = useState(props)
// initially, props.something is defined
// every time props changes (from the parent) props.something is redefined as expected and logged here
console.log(props.something)
// initially, props.something is rendered correctly
// every time props.something changes (from the parent) the HTML never updates
return (
{state.something && <div>{state.something}</div>}
)
}
I already tried using useEffect() even though I don't see the point, but it it didn't fix anything.
State will not update just because props change, this is why you need useEffect.
export default function(props) {
const [state, setState] = useState(props)
useEffect(() => {
setState(props.something)
}, [props.something])
// initially, props.something is defined
// every time props changes (from the parent) props.something is redefined as expected and logged here
console.log(props.something)
// initially, props.something is rendered correctly
// every time props.something changes (from the parent) the HTML never updates
return (<div>{state.something}</div>)
}
adding props.something to the array as the second argument to useEffect tells it to watch for changes to props.something and run the effect when a change was detected.
Update: In this specific example, there is no reason to copy props to state, just use the prop directly.
In your example you copy props to state only once, when initial values set.
It's almost never a good idea to copy props to component state though. You can read about it react docs
in this very simple demo
import { useState } from 'react';
function App() {
const [check, setCheck] = useState(false);
console.log('App component Init');
return (
<div>
<h2>Let's get started! </h2>
<button
onClick={() => {
setCheck(true);
}}
>
ClickMe
</button>
</div>
);
}
export default App;
i get one log on app init,
upon the first click (state changes from false to true) i get another log as expected.
But on the second click i also get a log , although the state remains the same.(interstingly the ReactDevTools doesn't produce the highlight effect around the component as when it is rerendered)
For every following clicks no log is displayed.
Why is this extra log happening.
Here is a stackblitz demo:
https://stackblitz.com/edit/react-ts-wvaymj?file=index.tsx
Thanks in advance
Given
i get one log on app init,
upon the first click (state changes from false to true) i get another
log as expected.
But on the second click i also get a log , although the state remains
the same.(interstingly the ReactDevTools doesn't produce the highlight
effect around the component as when it is rerendered)
For every following clicks no log is displayed.
And your question being
Why is this extra log happening?
Check the useState Bailing Out of a State Update section (emphasis mine):
If you update a State Hook to the same value as the current state,
React will bail out without rendering the children or firing effects.
(React uses the Object.is comparison algorithm.)
Note that React may still need to render that specific component again
before bailing out. That shouldn’t be a concern because React won’t
unnecessarily go “deeper” into the tree. If you’re doing expensive
calculations while rendering, you can optimize them with useMemo.
The answer to your question is essentially, "It's just the way React and the useState hook work." I'm guessing the second additional render is to check that no children components need to be updated, and once confirmed, all further state updates of the same value are ignored.
If you console log check you can see...
The first click you will get check is true -> check state change from false (init) => true (click) => state change => view change => log expected.
The second click => check state is true (from frist click) => true => state not change => view not render.
So. you can try
setCheck(!check);
I have a button that use this function to delete a value from firebase, these values are shown in a flatlist in the same page, the function works good but to see the changes I need to reload the page or go back and return on it.
This is my function
const onDelete = (sizeId) => {
firebase.firestore()
.collection("Measures")
.doc(firebase.auth().currentUser.uid)
.collection(route.params.size)
.doc(sizeId)
.delete()
How can I make it refresh after the changes so I can see the new flatlist without the value that I deleted?
If you use a React.Class you can try using a PureComponent. Upon removing the value from the state array, you could use a boolean state that you flip between true and false to trigger a re-render.
If you are using a functional component and you keep the data in a useState hook, you can cause a re-render via useEffect on that state upon change.
const [arr, setArr] = useState([]);
useEffect( () => {}, [arr] ); //This listens for state changes on arr and will cause a re-render upon change.
This would only work if you aren't setting state in your useEffect as it will cause an infinite loop if you call setArr() in your useEffect. In which case I have used a simple boolean that I flip back and forth when I want a re-render.
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.
I have a React Component using a hook to save the scroll position of the component when the component unmounts. This works great but fails when navigating from one set of data to another set of data without the component unmounting.
For instance, imagine the Slack Interface where there is a sidebar of message channels on the left and on the right is a list of messages (messageList). If you were to navigate between two channels, the messageList component would update with a new set of data for the messageList, but the component was never unmounted so scroll position never gets saved.
I came up with a solution that works, but also throws a warning.
My current useEffect hook for the component (stripped down) and the code that currently saves scroll position whenever the messageList ID changes:
// Component...
const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
// Save scroll position when Component unmounts
useEffect(() => {
return () => {
setScrollOffset(parent._id, scrollPos.current);
};
}, []);
// Save scroll position when Parent ID changes
const oldParent = usePrevious(parent);
if (oldParent && parent._id !== oldParent._id) {
setScrollOffset(oldParent._id, list ? list.scrollTop : 0);
}
// ...Component
The error this throws is:
Warning: Cannot update a component from inside the function body of a different component.
And the line that is causing it is the setScrollOffset call inside of the last if block. I'm assuming that while this works it is not the way that I should be handling this sort of thing. What is a better way to handle saving scroll position when a specific prop on the component changes?
Add parent._id to the dependency array. Refactor your code to still cache the previous parent id, add that to the dependency, and move the conditional test inside the effect.
Cleaning up an effect
The clean-up function runs before the component is removed from the UI
to prevent memory leaks. Additionally, if a component renders multiple
times (as they typically do), the previous effect is cleaned up before
executing the next effect.
// Return previous parent id and cache current
const oldParent = usePrevious(parent);
// Save scroll position when Component unmounts or parent id changes
useEffect(() => {
if (oldParent && parent._id !== oldParent._id) {
setScrollOffset(oldParent._id, list ? list.scrollTop : 0);
}
return () => {
setScrollOffset(parent._id, scrollPos.current);
};
}, [parent._id, oldParent]);
If this does't quite fit the bill, then use two effects, one for the mount/unmount and the other for just updates on the parent id.
Thanks to the suggestions of #drew-reese, he got me pointed down the right path. After adopting his solution (which previously I could not get working properly), I was able to isolate my problem to usage with react-router. (connected-react-router in my case). The issue was that the component was rendering and firing the onScroll event handler and overwriting my scroll position before I could read it.
For me the solution ended up being to keep my existing useEffect hook but pull the scroll offset save out of it and into useLayoutEffect (Had to keep useEffect since there is other stuff in useEffect that I removed for the sake of keeping the sample code above lean). useLayoutEffect allowed me to read the current scroll position before the component fired the onScroll event which was ultimately overwriting my saved scroll position reference to 0.
This actually made my code much cleaner overall by removing the need for my usePrevious hook entirely. My useLayoutEffect hook now looks like this:
useLayoutEffect(() => {
return () => {
setScrollOffset(parent._id, scrollPos.current);
};
}, [parent._id]);