I define a scene: we have a component that uses parent's props and itself state.
There are two Components DC and JOKER and my step under the below:
click DC's button
DC setCount
JOKER will render with the old state
running useEffect and setCount
JOKER does render again
I want to ask why JOKER render twice(step 3 and 5) and the first render squanders the performance. I just do not want step 3. If in class component I can use componentShouldUpdate to avoid it. But Hooks has the same something?
My code under the below, or open this website https://jsfiddle.net/stephenkingsley/sw5qnjg7/
import React, { PureComponent, useState, useEffect, } from 'react';
function JOKER(props) {
const [count, setCount] = useState(props.count);
useEffect(() => {
console.log('I am JOKER\'s useEffect--->', props.count);
setCount(props.count);
}, [props.count]);
console.log('I am JOKER\'s render-->', count);
return (
<div>
<p style={{ color: 'red' }}>JOKER: You clicked {count} times</p>
</div>
);
}
function DC() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => {
console.log('\n');
setCount(count + 1);
}}>
Click me
</button>
<JOKER count={count} />
</div>
);
}
ReactDOM.render(<DC />, document.querySelector("#app"))
It's an intentional feature of the StrictMode. This only happens in
development, and helps find accidental side effects put into the
render phase. We only do this for components with Hooks because those
are more likely to accidentally have side effects in the wrong place.
-- gaearon commented on Mar 9, 2019
You can simply make modifications in ./index.js
change this
<React.StrictMode>
<App />
</React.StrictMode>
to this
<>
<App />
</>
React.StrictMode causes component render in development mode. (works in reactjs version 18.0.2)
I'm not sure I understand your question, but here goes.
When your <DC /> component changes state, it passes the new state value count to the component Joker. At this point the component will rerender, accounting for the first change.
Then you bind the effect to props.count changes;
useEffect(() => {
console.log('I am JOKER\'s useEffect--->', props.count);
setCount(props.count);
}, [props.count]);// <-- This one
Which triggers when the component gets the new value from the component DC. It will set the state of it self Joker to props.count, which causes the component to rerender.
Which then gives you the following output:
I am JOKER's render--> 1 // Initial render where Joker receives props from DC
index.js:27 I am JOKER's useEffect---> 2 // The hook runs because props.count changed
index.js:27 I am JOKER's render--> 2 // Joker rerenders because its state updated.
If we just want to do the same something alike componentShouldUpdate, we can use useMemo.
function DC() {
const [count, setCount] = useState(0);
const [sum, setSum] = useState(0);
const memoizedJOKER = useMemo(() => <JOKER count={count} />, [count]);
return (
<div>
<button onClick={() => {
// setCount(count + 1);
setSum(sum + 1);
console.log('---click---');
console.log('\n');
}}>
Click me
</button>
<p>DC: You clicked {count} times</p>
<p>now this is {sum} times</p>
{memoizedJOKER}
</div>
);
}
When you click button, JOKER does not render again.
Use the following custom useEffect code to force react to render a component once, all you need to do is import and use it in place of usEffect.
import {useRef} from 'react'
export const useEffectOnce = ( effect )=> {
const destroyFunc = useRef();
const effectCalled = useRef(false);
const renderAfterCalled = useRef(false);
const [val, setVal] = useState(0);
if (effectCalled.current) {
renderAfterCalled.current = true;
}
useEffect( ()=> {
// only execute the effect first time around
if (!effectCalled.current) {
destroyFunc.current = effect();
effectCalled.current = true;
}
// this forces one render after the effect is run
setVal(val => val + 1);
return ()=> {
// if the comp didn't render since the useEffect was called,
// we know it's the dummy React cycle
if (!renderAfterCalled.current) { return; }
if (destroyFunc.current) { destroyFunc.current(); }
};
}, []);
};
For more information click here
Related
This is a very common performance problem while using the Context API. Essentially whenever a state value in the context changes, the entire components that are wrapped between the provider re-renders and causes performance slowdown.
If I have a the wrapper as this:
<CounterProvider>
<SayHello />
<ShowResult />
<IncrementCounter />
<DecrementCounter />
</CounterProvider>
And the value props as:
<CounterContext.Provider value={{increment, decrement, counter, hello }} >
{children}
</CounterContext.Provider>
Everytime I increment the count value from the IncrementCounter component, the entire set of wrapped components re-renders as it is how the Context API is supposed to work.
I did a bit of research and came across these solutions:
Split the Context into N number of Context according to the use-case : This solution works as expected.
Wrap the value provider using React.Memo: I saw a lot of articles suggesting to the React.Memo API as follows:
<CounterContext.Provider
value={useMemo(
() => ({ increment, decrement, counter, hello }),
[increment, decrement, counter, hello]
)}
>
{children}
</CounterContext.Provider>
This however doesn't work as expected. I still can see all the components getting re-rendered. What I'm doing wrong while using the Memo API? Dan Abramov does recommend to go by this approach in an open React issue
If anyone can help me out on this one. Thanks for reading.
"Essentially whenever a state value in the context changes, the entire components that are wrapped between the provider re-renders and causes performance slowdown."
The above statement is true if a context is used like in the below example where components are directly nested in the provider. All of them re-render when count changes, no matter wether they are called useContext(counterContext) or not.
const counterContext = React.createContext();
const CounterContextProvider = () => {
const [count, setCount] = React.useState(0);
return (
<counterContext.Provider value={{ count, setCount }}>
<button onClick={() => setCount((prev) => prev + 1)}>Change state</button>
<ComponentOne/>
<ComponentTwo />
</counterContext.Provider>
);
};
const ComponentOne = () => {
console.log("ComponentOne renders");
return <div></div>;
};
const ComponentTwo = () => {
console.log("ComponentTwo renders ");
return <div></div>;
};
function App() {
return (
<CounterContextProvider/>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
"Essentially whenever a state value in the context changes, the entire components that are wrapped between the provider re-renders and causes performance slowdown."
The statement is false if you are consuming nested components with children. This time when count changes CounterContextProvider renders, but since it's rendering because its state has changed and not because of its parent rendering, and because a component cannot mutate its props, React won't render children. That's it if it was a normal component.
But since there is a context involved here, React will find all components that contain useContext(counterContext) and render them.
const counterContext = React.createContext();
const CounterContextProvider = ({ children }) => {
const [count, setCount] = React.useState(0);
return (
<counterContext.Provider value={{ count, setCount }}>
<button onClick={() => setCount((prev) => prev + 1)}>Change state</button>
{children}
</counterContext.Provider>
);
};
const ComponentOne = () => {
const { count } = React.useContext(counterContext);
console.log("ComponentOne renders");
return <div></div>;
};
const ComponentTwo = () => {
console.log("ComponentTwo renders ");
return <div></div>;
};
function App() {
return (
<CounterContextProvider>
<ComponentOne />
<ComponentTwo />
</CounterContextProvider>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
In the above example only ComponentOne renders when count changes, which is normal cause he is consuming it. Every component that calls useContext(counterContext) renders if one value of the context changes.
Even with useMemo wrapping the context object as you did, that's the behavior you get as soon as one variable in its dependency array changes.
Let say we have parent and children components like this:
const {useState, useCallback} = React;
const ComponentB = (props) => {
const [text, setText] = useState('')
const { onClick } = props
const handleChange = useCallback((event) => {
setText(event.target.value)
}, [text])
const handleClick = useCallback(() => {
onClick(text)
}, [onClick, text]) // Should I to take into account 'onClick' props?
return (
<div>
<input type="text" onChange={ handleChange } />
<button type="button" onClick={ handleClick }>Save</button>
</div>
)
}
const ComponentA = () => {
const [stateA, setStateA] = useState('')
const handleSetStateA = useCallback((state) => {
setStateA(state)
}, [stateA])
return (
<div>
<ComponentB onClick={ handleSetStateA } />
{ stateA && `${ stateA } saved!` }
</div>
)
}
ReactDOM.createRoot(
document.getElementById("root")
).render(
<ComponentA />
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
React documentation says that:
every value referenced inside the callback should also appear in the dependencies array
And I'm wondering if I need to put onClick method to array dependencies in useCallback? And if so, why I should do that?
And I'm wondering if I need to put onClick method to array dependencies in useCallback?
Yes.
And if so, why I should do that?
Because it's possible that your component will get re-rendered with a new and different function for the onClick prop that behaves differently from the old one. Without saying it's a dependency, you'll continue using the old value.
In fact, in your given code, it's not just possible but definite: you create a new handleSetStateA function every time stateA changes.
That said, in ComponentA:
There's no reason to have stateA as a dependency in your useCallback creating handleSetStateA; handleSetStateA never uses stateA. (It uses the state setter function for it, but that's not the same thing.)
There's not really any reason for handleSetStateA at all; just pass setStateA directly as onClick. But I'm assuming you do more than just setting the state in that function and just left things out for the purposes of the question.
(Similarly, in ComponentB there's no reason for text to be a dependency on the useCallback for handleChange; handleChange doesn't use text.)
But even if you change ComponentA to pass setStateA directly (or at least provide a stable function), ComponentB shouldn't rely on onClick being unchanging between renders, so you'd use onClick in your useCallback dependency list regardless.
Finally: There's not much point in using useCallback with functions you're passing to unmemoized components. For it to be useful, the component you're providing the callback function to should be memoized (for instance, via React.memo or, for a class component, via shouldComponentUpdate). See my answer here for details on that.
Here's an updated version of your snippet using React.memo and only the necessary dependencies; I've left handleSetStateA in (I added a console.log so it isn't just a wrapper):
const { useState, useCallback } = React;
const ComponentB = React.memo(({ onClick }) => {
const [text, setText] = useState("");
const handleChange = useCallback((event) => {
setText(event.target.value);
}, []);
const handleClick = useCallback(() => {
console.log(`Handling click when text = "${text}"`);
onClick(text);
}, [onClick, text]);
return (
<div>
<input type="text" onChange={handleChange} />
<button type="button" onClick={handleClick}>
Save
</button>
</div>
);
});
const ComponentA = () => {
const [stateA, setStateA] = useState("");
const handleSetStateA = useCallback((state) => {
console.log(`Setting stateA to "${state}"`);
setStateA(state);
}, []);
return (
<div>
<ComponentB onClick={handleSetStateA} />
{stateA && `${stateA} saved!`}
</div>
);
};
ReactDOM.createRoot(document.getElementById("root")).render(<ComponentA />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
Consider the following example:
import "./styles.css";
import React from "react";
function Children(props) {
const counter = props.counter2;
console.log("re-rendering children");
return (
<div className="App">
<p>counter2 value = {counter} </p>
</div>
);
}
export default function App() {
const [counter, setCounter] = React.useState(0);
const [counter2] = React.useState(0);
return (
<div className="App">
<p>counter value = {counter} </p>
<button onClick={(e) => setCounter((c) => c + 1)}>increment</button>
<Children counter2={counter2} />
</div>
);
}
The Children component does not depend on the parent state counter, but when I click the button I can see that re-rendering log printed every time I click. If I am not mistaken then it means the Children component is re-rendered. My question is why? It should not re-render as the props did not change.
code sandbox: https://codesandbox.io/s/modern-dawn-xj9b8
Because of the lifecycle of React, when you are using a functional component, it is also considered stateless and will re-render every time there is an update in its parent component, whether there is a change in props passing into the child.
There are two ways you can do to change this behaviour,
use class component and decide whether to update it using shouldComponentUpdate
or the recommended way using React.memo,
import "./styles.css";
import React from "react";
function Children(props) {
const counter = props.counter2;
console.log("re-rendering children");
return (
<div className="App">
<p>counter2 value = {counter} </p>
</div>
);
}
const MemoChild = React.memo(Children);
export default function App() {
const [counter, setCounter] = React.useState(0);
const [counter2] = React.useState(0);
return (
<div className="App">
<p>counter value = {counter} </p>
<button onClick={(e) => setCounter((c) => c + 1)}>increment</button>
<MemoChild counter2={counter2} />
{/* <Children counter2={counter2} /> */}
</div>
);
}
More on React.memo in the docs
If your component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.
this answer is true that you can use React.memo but before start using, check some articles to figure out when it's valuable to use it.
using it inappropriate could damage your performance.
I am trying to figure out why when I click on a specific component its sibling it will render too
function CountButton({increment, count, number}) {
console.log(`Render CountButton ${number}`)
return <button onClick={() => increment(count + 1)}>{count}</button>
}
function DualCounter() {
const [count1, setCount1] = React.useState(0)
const increment1 = React.useCallback(() => setCount1(c => c + 1), [])
const [count2, setCount2] = React.useState(0)
const increment2 = React.useCallback(() => setCount2(c => c + 1), [])
console.log('Render DualCounter')
return (
<>
<CountButton count={count1} increment={increment1} number={1} />
<CountButton count={count2} increment={increment2} number={2} />
</>
)
}
I use useCallback and I pass theses function to use avoid that in any render the functions reference will be a different reference.
You are seeing a re-render on the sibling <CountButton /> component, because each time you hit the button to update the counter, you are actually updating a state value in the parent component <DualCounter />, which causes a re-render on that component as well.
And since DualCounter is re-rendered, child components will re-render as well, which in this case includes both <CountButton /> elements.
A solution to prevent this, would be wrapping CountButton component with React.memo(). This will prevent a re-render on a component that didn't have any change on the props values.
Example below:
function CountButton({increment, count, number}) {
console.log(`Render CountButton ${number}`)
return <button onClick={() => increment(count + 1)}>{count}</button>
}
const CountButtonMemo = React.memo(CountButton)
function DualCounter() {
const [count1, setCount1] = React.useState(0)
const increment1 = React.useCallback(() => setCount1(c => c + 1), [])
const [count2, setCount2] = React.useState(0)
const increment2 = React.useCallback(() => setCount2(c => c + 1), [])
console.log('Render DualCounter')
return (
<>
<CountButtonMemo count={count1} increment={increment1} number={1} />
<CountButtonMemo count={count2} increment={increment2} number={2} />
</>
)
Another solution would be not updating the DualCounter state on each change caused by events on your CountButton components, which will stop triggering unwanted re-renders on their siblings. You could handle the state directly on each CountButton component if this made sense for your app.
Alternatively, you could use a React state management tool, such as Redux, which also solves exactly this issue, by taking charge of delegating the state of your app separated from your components themselves.
I want to save state to localStorage when a component is unmounted.
This used to work in componentWillUnmount.
I tried to do the same with the useEffect hook, but it seems state is not correct in the return function of useEffect.
Why is that? How can I save state without using a class?
Here is a dummy example. When you press close, the result is always 0.
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
function Example() {
const [tab, setTab] = useState(0);
return (
<div>
{tab === 0 && <Content onClose={() => setTab(1)} />}
{tab === 1 && <div>Why is count in console always 0 ?</div>}
</div>
);
}
function Content(props) {
const [count, setCount] = useState(0);
useEffect(() => {
// TODO: Load state from localStorage on mount
return () => {
console.log("count:", count);
};
}, []);
return (
<div>
<p>Day: {count}</p>
<button onClick={() => setCount(count - 1)}>-1</button>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => props.onClose()}>close</button>
</div>
);
}
ReactDOM.render(<Example />, document.querySelector("#app"));
CodeSandbox
I tried to do the same with the useEffect hook, but it seems state is not correct in the return function of useEffect.
The reason for this is due to closures. A closure is a function's reference to the variables in its scope. Your useEffect callback is only ran once when the component mounts and hence the return callback is referencing the initial count value of 0.
The answers given here are what I would recommend. I would recommend #Jed Richard's answer of passing [count] to useEffect, which has the effect of writing to localStorage only when count changes. This is better than the approach of not passing anything at all writing on every update. Unless you are changing count extremely frequently (every few ms), you wouldn't see a performance issue and it's fine to write to localStorage whenever count changes.
useEffect(() => { ... }, [count]);
If you insist on only writing to localStorage on unmount, there's an ugly hack/solution you can use - refs. Basically you would create a variable that is present throughout the whole lifecycle of the component which you can reference from anywhere within it. However, you would have to manually sync your state with that value and it's extremely troublesome. Refs don't give you the closure issue mentioned above because refs is an object with a current field and multiple calls to useRef will return you the same object. As long as you mutate the .current value, your useEffect can always (only) read the most updated value.
CodeSandbox link
const {useState, useEffect, useRef} = React;
function Example() {
const [tab, setTab] = useState(0);
return (
<div>
{tab === 0 && <Content onClose={() => setTab(1)} />}
{tab === 1 && <div>Count in console is not always 0</div>}
</div>
);
}
function Content(props) {
const value = useRef(0);
const [count, setCount] = useState(value.current);
useEffect(() => {
return () => {
console.log('count:', value.current);
};
}, []);
return (
<div>
<p>Day: {count}</p>
<button
onClick={() => {
value.current -= 1;
setCount(value.current);
}}
>
-1
</button>
<button
onClick={() => {
value.current += 1;
setCount(value.current);
}}
>
+1
</button>
<button onClick={() => props.onClose()}>close</button>
</div>
);
}
ReactDOM.render(<Example />, document.querySelector('#app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
This will work - using React's useRef - but its not pretty:
function Content(props) {
const [count, setCount] = useState(0);
const countRef = useRef();
// set/update countRef just like a regular variable
countRef.current = count;
// this effect fires as per a true componentWillUnmount
useEffect(() => () => {
console.log("count:", countRef.current);
}, []);
}
Note the slightly more bearable (in my opinion!) 'function that returns a function' code construct for useEffect.
The issue is that useEffect copies the props and state at composition time and so never re-evaluates them - which doesn't help this use case but then its not what useEffects are really for.
Thanks to #Xitang for the direct assignment to .current for the ref, no need for a useEffect here. sweet!
Your useEffect callback function is showing the initial count, that is because your useEffect is run only once on the initial render and the callback is stored with the value of count that was present during the iniital render which is zero.
What you would instead do in your case is
useEffect(() => {
// TODO: Load state from localStorage on mount
return () => {
console.log("count:", count);
};
});
In the react docs, you would find a reason on why it is defined like this
When exactly does React clean up an effect? React performs the cleanup when the component unmounts. However, as we learned earlier,
effects run for every render and not just once. This is why React also
cleans up effects from the previous render before running the effects
next time.
Read the react docs on Why Effects Run on Each Update
It does run on each render, to optimise it you can make it to run on count change. But this is the current proposed behavior of useEffect as also mentioned in the documentation and might change in the actual implementation.
useEffect(() => {
// TODO: Load state from localStorage on mount
return () => {
console.log("count:", count);
};
}, [count]);
The other answer is correct. And why not pass [count] to your useEffect, and so save to localStorage whenever count changes? There's no real performance penalty calling localStorage like that.
Instead of manually tracking your state changes like in the accepted answer you can use useEffect to update the ref.
function Content(props) {
const [count, setCount] = useState(0);
const currentCountRef = useRef(count);
// update the ref if the counter changes
useEffect(() => {
currentCountRef.current = count;
}, [count]);
// use the ref on unmount
useEffect(
() => () => {
console.log("count:", currentCountRef.current);
},
[]
);
return (
<div>
<p>Day: {count}</p>
<button onClick={() => setCount(count - 1)}>-1</button>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => props.onClose()}>close</button>
</div>
);
}
What's happening is first time useEffect runs, it creating a closure over the value of state you're passing; then if you want to get the actual rather the first one.. you've two options:
Having the useEffect with a dependency over count, which will refresh it on each change on that dependency.
Use function updater on setCount.
If you do something like:
useEffect(() => {
return () => {
setCount((current)=>{ console.log('count:', current); return current; });
};
}, []);
I am adding this solution just in case someone comes here looking for an issue trying to do an update based on old value into a useEffect without reload.