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);
Related
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"));
I'm trying to get a functional component to force a rerender whenever the testFn executes. I thought to use state to do this (if there's a better way then please speak up), which appears to successfully force a rerender but only twice, then nothing.
I built a simple demo to emulate the issue as using my real app is too difficult to demonstrate but the same principles should presumably apply (my real demo fetches data when the function executes and displays it on the page, but it's not rerendering and I have to refresh the page to see the new data, hence why I want to trigger a rerender).
import React, { useState } from "react";
const App = () => {
const [, rerender] = useState(false);
const testFn = () => {
console.log("test Fn");
rerender(true);
};
return (
<div>
<p>test</p>
<button onClick={testFn}>clickk</button>
{console.log("render")}
</div>
);
};
export default App;
I've also made a Stackblitz demo for conveinence.
Can anyone solve this demo or think of a better way of implementing it?
Thanks for any help here.
It triggers a re-render when the state changes.
The first time you click the button you change the state from false to true so a rerender is triggered.
Subsequent clicks you change it from true to true which isn't a change, so it doesn't.
You could toggle it:
const [render, rerender] = useState(false);
and
rerender(!render);
so it actually changes.
… but this smells of being an XY Problem, and you should probably be changing something which is actually being rendered.
State is the right way, since state changes are the primary way to cause re-renders. I'd increment a counter:
const [, setCount] = useState(0);
// force a re-render by running:
setCount(c => c + 1)
But this is still a very odd thing to do. In almost all cases, a much more elegant solution will be implementable, such as by putting data that changes into state, and calling the state setter when the data updates.
I've just started learning React and was putting together a small app which makes calls to a quotes API. The API has an endpoint that returns a random quote. When the app initially loads it makes a call to the API and shows a quote, and there's a button that can be clicked to get a new random quote (new call to the API).
I have a root component named App. This component has a QuoteWrap component as a child. The QuoteWrap component has two children: the button that is used to get a new random quote and a Quote component which shows the author of the quote and the quote itself. This is the code inside of the QuoteWrap component:
export default function QuoteWrap() {
const { quoteData, isLoading, fireNewCall } = useQuote();
const handleClick = () => {
fireNewCall();
};
return(
<>
<button onClick={handleClick}>Get random quote</button>
{ isLoading ?
<h2>Loading...</h2>
:
<Quote author={quoteData.author} quote={quoteData.quote} />
}
</>
);
}
useQuote() is a custom hook that manages the calls to the API and returns 3 values: 1- the data, 2- if a call is in process and 3- a function to make a call to the API.
Obviously, every time the button is clicked, the whole QuoteWrap component is re-rendered (as quoteData and isLoading change). But really, the button doesn't need to be re-rendered as it never changes.
So I thought: ok, I can move the button up to the App component. But then I don't have access to the fireNewCall function in the useQuote hook.
How can I prevent the button from being re-rendered? Is it even important in this case or am I getting too obsessed with React re-renders?
Thanks!
Your component will re-render every time the handleClick function changes, which is every time that QuoteWrap is rendered.
The solution is the useCallback hook. useCallback will return the same function to handleClick, every time QuoteWrap is rendered, so long as the dependencies haven't changed.
https://reactjs.org/docs/hooks-reference.html#usecallback
You would use it like this:
const handleClick = useCallback(() => {
fireNewCall();
},[fireNewCall]);
fireNewCall is the dependency, so as long as useQuote returns a stable fireNewCall function, then your button will not re-render, since the handleClick property hasn't changed.
I think you might get too obsessed with React re-renders. The button should be re-rendered because, handleClick should be changed when fireNewCall changed for some case. Even if, handleClick will never be changed. It's no need to think about an element re-render.
Pretty much what Benjamin and Viet said - in your original code, a new function is assigned to handleClick on each render. You can use React.useCallback to maintain the original function reference and only update it when something in the dependency array changes - in this case, just fireNewCall needs to go into the dependency array.
But as Viet says, don't get too obsessed with it. Using React.useCallback might even slow down your code. Check out Kent C. Dodds When to useMemo and useCallback post for more insight.
Am getting this warning:
Can't perform a React state update on unmounted component. This is a no-op...
It results from a child component and I can't figure out how to make it go away.
Please note that I have read many other posts about why this happens, and understand the basic issue. However, most solutions suggest cancelling subscriptions in a componentWillUnmount style function (I'm using react hooks)
I don't know if this points to some larger fundamental misunderstanding I have of React,but here is essentially what i have:
import React, { useEffect, useRef } from 'react';
import Picker from 'emoji-picker-react';
const MyTextarea = (props) => {
const onClick = (event, emojiObject) => {
//do stuff...
}
const isMountedRef = useRef(true);
useEffect(() => {
isMountedRef.current = true;
});
useEffect(() => {
return () => {
console.log('will unmount');
isMountedRef.current = false;
}
});
return (
<div>
<textarea></textarea>
{ isMountedRef.current ? (
<Picker onEmojiClick={onClick}/>
):null
}
</div>
);
};
export default MyTextarea;
(tl;dr) Please note:
MyTextarea component has a parent component which is only rendered on a certain route.
Theres also a Menu component that once clicked, changes the route and depending on the situation will either show MyTextarea's parent component or show another component.
This warning happens once I click the Menu to switch off MyTextarea's parent component.
More Context
Other answers on StackOverflow suggest making changes to prevent state updates when a component isn't mounted. In my situation, I cannot do that because I didn't design the Picker component (rendered by MyTextarea). The Warning originates from this <Picker onEmojiClick={onClick}> line but I wouldn't want to modify this off-the-shelf component.
That's explains my attempt to either render the component or not based on the isMountedRef. However this doesn't work either. What happens is the component is either rendered if i set useRef(true), or it's never rendered at all if i set useRef(null) as many have suggested.
I'm not exactly sure what your problem actually is (is it that you can't get rid of the warning or that the <Picker> is either always rendering or never is), but I'll try to address all the problems I see.
Firstly, you shouldn't need to conditionally render the <Picker> depending on whether MyTextArea is mounted or not. Since components only render after mounting, the <Picker> will never render if the component it's in hasn't mounted.
That being said, if you still want to keep track of when the component is mounted, I'd suggest not using hooks, and using componentDidMount and componentWillUnmount with setState() instead. Not only will this make it easier to understand your component's lifecycle, but there are also some problems with the way you're using hooks.
Right now, your useRef(true) will set isMountedRef.current to true when the component is initialized, so it will be true even before its mounted. useRef() is not the same as componentDidMount().
Using 'useEffect()' to switch isMountedRef.current to true when the component is mounted won't work either. While it will fire when the component is mounted, useEffect() is for side effects, not state updates, so it doesn't trigger a re-render, which is why the component never renders when you set useRef(null).
Also, your useEffect() hook will fire every time your component updates, not just when it mounts, and your clean up function (the function being returned) will also fire on every update, not just when it unmounts. So on every update, isMountedRef.current will switch from true to false and back to true. However, none of this matters because the component won't re-render anyways (as explained above).
If you really do need to use useEffect() then you should combine it into one function and use it to update state so that it triggers a re-render:
const [isMounted, setIsMounted] = useState(false); // Create state variables
useEffect(() => {
setIsMounted(true); // The effect and clean up are in one function
return () => {
console.log('will unmount');
setIsMounted(false);
}
}, [] // This prevents firing on every update, w/o it you'll get an infinite loop
);
Lastly, from the code you shared, your component couldn't be causing the warning because there are no state updates anywhere in your code. You should check the picker's repo for issues.
Edit: Seems the warning is caused by your Picker package and there's already an issue for it https://github.com/ealush/emoji-picker-react/issues/142
I am using useEffect to render a modal if a query string shows up that contains "newcc"
useEffect(() => {
if (qs === "newcc") {
setShowPaymentModal(true)
}
}, [qs]);
Any time I click on a button for a previous modal that directs to the account page with newCC if I am already on that page it will not rerender that modal (makes sense that useEffect isn't called again):
<Button text="Update Payment Info"
onClick={() => {
history.push({
pathname: '/account',
search: 'alert=newcc',
})
}}
/>
A workaround I have found is to simply include the following in the dependency array:
history.location.key
This forces a re-render since the key changes every push. The question I have is that this brings up the lint error of:
React Hook useEffect has an unnecessary dependency:
'history.location.key'. Either exclude it or remove the dependency
array. Outer scope values like 'history.location.key' aren't valid
dependencies because mutating them doesn't re-render the component
Is it safe to simply ignore this lint error or am I approaching this situation the wrong way? It does re-render the component. I hate to ignore lint errors if I don't have to.
It sounds like the issue is that your trying to capture too much logic in one place and it's simpler to manage this by separating things out.
state should contain
isModalOpen: Boolean
&& the render of a component should contain the logic check for pathname.
in render / functional component's return
{qs === "newcc" ? return newCC : null}
{...rest of component}