Can not get latest state in React hook | Stale Closure Issue - reactjs

I have a React app, there's a count increase every second, click on the button will alert the latest count after 2 seconds.
expect: click on the button at 10 seconds, the alert should display 12 (the latest count after 2 seconds).
actual: click on the button at 10 seconds, the alert displays 10 (stale closure issue)
I solved this issue by using "Class component" but I want to understand the Stale closure issue of "Function component".
function App() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
setInterval(() => {
setCount(c => c + 1);
}, 1000);
}, []);
const showCurrentCount = () => {
setTimeout(() => {
alert(count);
}, 2000);
};
return (
<div>
<h1>{count}</h1>
<button onClick={() => showCurrentCount()}>show</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://unpkg.com/react#17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>

You can fix the stale value issue by using the callback method of setState. Inside the callback, alert the count return the same count.
const showCurrentCount = () => {
setTimeout(() => {
setCount((count) => {
alert(count);
return count;
});
}, 2000);
};
Demo
function App() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
setInterval(() => {
setCount(c => c + 1);
}, 1000);
}, []);
const showCurrentCount = () => {
setTimeout(() => {
setCount((count) => {
alert(count);
return count;
});
}, 2000);
};
return (
<div>
<h1>{count}</h1>
<button onClick={() => showCurrentCount()}>show</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://unpkg.com/react#17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>

I just fixed my issue by using useRef.
React documentation:
https://reactjs.org/docs/hooks-reference.html#useref
function App() {
const refCount = React.useRef();
const [count, setCount] = React.useState(0);
refCount.current = count;
React.useEffect(() => {
setInterval(() => {
setCount(c => c + 1);
}, 1000);
}, []);
const showCurrentCount = () => {
setTimeout(() => {
alert(refCount.current);
}, 2000);
};
return (
<div>
<h1>{count}</h1>
<button onClick={showCurrentCount}>show</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://unpkg.com/react#17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>

Related

React ref.current is null in useEffect cleanup function [duplicate]

This question already has answers here:
Cleanup ref issues in React
(2 answers)
Closed 10 months ago.
I'm trying to access a ref during clean up (before the component unmounts).
Like so:
const Comp = () => {
const imgRef = React.useRef();
React.useEffect(() => {
console.log('component mounted', imgRef); // During mount, imgRef.current is always defined
return () => {
console.log('component unmounting', imgRef); // imgRef.current always null here
}
}, []); // also tried adding imgRef and imgRef.current still doesn't work in clean up
return (
<img src={'example.png'} ref={imgRef} />
);
};
const App = () => {
const [img, setImg] = React.useState(true);
return <div>
<button onClick={() => setImg(!img)}>toggle</button>
{img && <Comp />}
</div>;
};
ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div class='react'></div>
Even adding imgRef in the useEffect's dependency, the imgRef.current is still null in the return of useEffect...
This works in the equivalent Class component with componentWillUnmount the imgRef is properly defined.
How can I make it work with hooks?
This was very helpful: https://stackoverflow.com/a/67069936
Something like this worked for me:
const imgRef = useRef();
useEffect(() => {
let localRef = null;
if (imgRef.current) localRef = imgRef.current;
return () => {
console.log('component unmounting', localRef); // localRef works here!
}
}, []);
return (
<img ref={imgRef} src="example.png" />
);

Can I render piece of a stateful component in react?

Is there any api that allow us to write code something like this:
const MyComponents = () => {
const [number, setNumber] = useState(0);
return {
Btn: <Button onPress={() => setNumber(number + 1)}>
{number}
</Button>,
Log: <p>{number}</p>
}
}
const Perent = () => <>
<div ...>
<MyComponents.Btn/>
...
...
</div>
<MyComponents.Log/>
</>
Some kind of ability to group some Component.And render them in different places...
Seems like this would be better achieved by using a Context.
E.g.
const { createContext, useState, useContext } = React;
const CountContext = createContext();
const CountContainer = ({ children }) => {
const [number, setNumber] = useState(0);
return <CountContext.Provider value={{ number, setNumber }}>
{children}
</CountContext.Provider>
};
const CountButton = () => {
const { number, setNumber } = useContext(CountContext);
return <button onClick={() => setNumber((c) => c + 1)}>
{number}
</button>;
};
const CountLog = () => {
const { number } = useContext(CountContext);
return <p>{number}</p>;
};
const SomeCountButtons = () => <div><CountButton /><CountButton /></div>;
const App = () => (<div>
<CountContainer>
<CountButton />
<CountLog />
</CountContainer>
<CountContainer>
<SomeCountButtons />
<CountLog />
</CountContainer>
</div>);
ReactDOM.render(<App />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Then any <CountButton>s or <CountLog>s that occur anywhere within the same <CountContainer> will be able to share their state.

setState does not update inside intervalRef

I'm trying to learn how to use intervalRef where I increment the state ever 100 ms, but for some reason it does not work.
const {useState,useEffect,useRef} = React;
function Timer({active}) {
const intervalRef = useRef(null)
const [count, setCount] = useState(0)
useEffect(()=>{
if(active){
intervalRef.current = setInterval(()=>{
console.log(count);
setCount(count + 1);
},100)
} else {
clearInterval(intervalRef.current)
}
},[active])
return (
<p>{count}</p>
)
}
function Main() {
const [active, setActive] = useState(false)
return (
<div>
<Timer active={active}/>
<button onClick={()=>{setActive(!active)}}>Toggle</button>
</div>
)
}
ReactDOM.render(<Main />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="app"></div>
The interval works completely fine since the console.log(count) prints ok, but why doesn't setCount work?
Since the useEffect is not dependant on count, the count inside the closure is always 0, and 0 + 1 -> 1. Use an updater function when you call setState. The update function is called with the current state.
Note: you should also return a cleanup function from useEffect, that will clear the interval if the component is unmounted.
const { useState, useEffect, useRef } = React;
function Timer({ active }) {
const intervalRef = useRef(null);
const [count, setCount] = useState(0);
useEffect(
() => {
if (active) {
intervalRef.current = setInterval(() => {
setCount(count => count + 1);
}, 100);
} else {
clearInterval(intervalRef.current);
}
return () => clearInterval(intervalRef.current); // cleanup function
},
[active]
);
return <p>{count}</p>;
}
function Main() {
const [active, setActive] = useState(false);
return (
<div>
<Timer active={active} />
<button onClick={() => { setActive(!active); }}>Toggle</button>
</div>
);
}
ReactDOM.render(<Main />, document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="app"></div>

useState state update in callback referencing old state

const Child1 = (props) => {
const [obj, setObj] = React.useState({count: 1, enabled: true})
const onButtonClick = () => {
setObj({...obj, count: obj.count+1})
}
const onDelayedIncrement = () => {
setTimeout(() => {
setObj({...obj, count: obj.count+1})
}, 3000)
}
return (
<div>
<div>{obj.count}</div>
<button onClick={onButtonClick}>Increment</button>
<div><button onClick={onDelayedIncrement}>Delayed Increment</button></div>
</div>
);
};
ReactDOM.render(
<Child1 />,
document.getElementById('root')
);
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
In the above code if we click Delayed increment and later if we keep on clicking Increment, after setTimeout is executed and when setState is called it is using old state. How to solve this problem?
Use the functional form of setState:
setObj(currentObj => ({...currentObj, count: currentObj.count+1}))
More info in the official documentation.
Hooks related documentation.

How to update state conditionally in Stateless Component?

I have two components. MyButton and MyLabel component. I created 3 MyButton Compoents and 3 MyLabel Components. Each button has a different increment value. When you click on a button , the respective label should be updated not all the labels. At present all the labels are updating.
function MyButton(props) {
const onclick = () => {
props.onNumberIncrement(props.toBeIncremented);
};
return <button onClick={onclick}>+{props.toBeIncremented}</button>;
}
const MyLabel = function(props) {
return <label> {props.counter}</label>;
};
function App(props) {
const [counter, mySetCounter] = React.useState(0);
const handleClick = (incrementValue) => {
mySetCounter(counter + incrementValue);
};
return (
<div>
<MyButton
counter={counter}
onNumberIncrement={handleClick}
toBeIncremented={5}
/>
<MyButton
counter={counter}
onNumberIncrement={handleClick}
toBeIncremented={10}
/>
<MyButton
counter={counter}
onNumberIncrement={handleClick}
toBeIncremented={15}
/>
<br />
<MyLabel counter={counter} />
<MyLabel counter={counter} />
<MyLabel counter={counter} />
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
jsfiddle:
click here
Create a generator of button/label pairs with their local state and step. Generate the buttons and labels, and render them:
const useGenerateButtonAndLabel = step => {
const [counter, mySetCounter] = React.useState(0);
const onclick = React.useCallback(
() => mySetCounter(counter + step),
[step, counter]
);
return [
<button onClick={onclick}>+{step}</button>,
<label> {counter}</label>
];
};
function App(props) {
const [button1, label1] = useGenerateButtonAndLabel(5);
const [button2, label2] = useGenerateButtonAndLabel(10);
const [button3, label3] = useGenerateButtonAndLabel(15);
return (
<div>
{button1}
{button2}
{button3}
<br />
{label1}
{label2}
{label3}
</div>
);
}
ReactDOM.render(<App />, document.getElementById('demo'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="demo"></div>
If you also need a total, each generated pair can also return it's current counter, and you can sum them in the parent. In this example, I also automate the items creation/rendering with Array.from(), map, and reduce.
const useGenerateButtonAndLabel = step => {
const [counter, mySetCounter] = React.useState(0);
const onclick = React.useCallback(
() => mySetCounter(counter + step),
[step, counter]
);
// step is used here is a key, but if step is not unique, it will fail. You might want to generate a UUID here
return [
<button key={step} onClick={onclick}>+{step}</button>,
<label key={step}> {counter}</label>,
counter
];
};
const sum = items => items.reduce((r, [,, counter]) => r + counter, 0);
function App(props) {
const items = Array.from({ length: 5 },
(_, i) => useGenerateButtonAndLabel(5 * (i + 1))
);
return (
<div>
{items.map(([button]) => button)}
<br />
{items.map(([, label]) => label)}
<div>Total: {sum(items)}</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('demo'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="demo"></div>
Here is another solution using a single state variable :
function App(props) {
const [appState, setAppState] = React.useState([
{value: 0, incrementValue: 5},
{value: 0, incrementValue: 10},
{value: 0, incrementValue: 15}
]);
const handleClick = clickedIndex => () => {
setAppState(
appState.map((item, index) => clickedIndex !== index ?
item : ({...item, value: item.value + item.incrementValue})
)
);
};
return (
<div>
{
appState.map(({value, incrementValue}, index) => (
<MyButton
key={index}
counter={value}
onNumberIncrement={handleClick(index)}
toBeIncremented={incrementValue}
/>
));
}
<br />
{
appState.map(
({value}, index) => <MyLabel key={index} counter={value} />
);
}
</div>
);
}

Resources