useState state update in callback referencing old state - reactjs

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.

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 not get latest state in React hook | Stale Closure Issue

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>

useMemo behaves different for inline component vs. normal component

I have the following React code
const { useState, useMemo, Fragment } = React;
function Rand() {
return <Fragment>{Math.random()}</Fragment>;
}
const App = () => {
const [show, setShow] = useState(true);
// The inline component gets memoized. But <Rand /> does not
const working = useMemo(() => <Fragment>{Math.random()}</Fragment>, []);
// The rand component is not memoized and gets rerendred
const notWorking = useMemo(() => <Rand />, []);
return(
<Fragment>
<button
onClick={() => {
setShow(!show);
}}>
{show?"Hide":"Show"}
</button>
<br />
Working:
{show && working}
<br />
Not Working:
{show && notWorking}
</Fragment>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
It uses useMemo 2 times.
The first time it uses an inline component to "initialize" and memoize a component ( const working = useMemo(() => <>{Math.random()}</>, []);)
The second time it uses a component which was made outside the app component (const notWorking = useMemo(() => <Rand />, []);)
Both components used in the useMemo function have the exact same code, which is <>{Math.random()}</>.
Here comes the unexpected part, when I hide (Click the button) and show the two memoized components again, they behave differently. The first one will always show the same random number which it got when it first got initialzied. While the seconds one will re-initialize each time.
First render
Second render (hide)
Third render (show again)
You can see from the screenshots that the first component's random number stays the same, while the second one does not.
My Questions:
How can I prevent in both cases to re-render/re-initialize the component?
Why does it currently behave as it does?
Interestingly, it does get memoized if I use a counter instead of show/hide:
const { useState, useMemo, Fragment } = React;
function Rand() {
return <Fragment>{Math.random()}</Fragment>;
}
const App = () => {
const [counter, setCounter] = useState(0);
// The inline component gets memoized. But <Rand /> does not
const working = useMemo(() => <Fragment>{Math.random()}</Fragment>, []);
// The rand component is not memoized and gets rerendred
const notWorking = useMemo(() => <Rand />, []);
return(
<Fragment>
<button
onClick={() => {
setCounter(c => c + 1);
}}>
Update ({counter})
</button>
<br />
Working:
{working}
<br />
Not Working:
{notWorking}
<br />
<code>Rand</code> used directly:
<Rand />
</Fragment>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
Here is a codepen to try it yourself https://codepen.io/brandiatmuhkuh/pen/eYWmyWz
Why does it currently behave as it does?
<Rand /> doesn't call your component function. It just calls React.createElement to create the React element (not an instance of it). Your component function is used to render an element instance, if and when you use it. In your "working" example you're doing:
<>{Math.random()}</>
...which calls Math.random and uses its result as text (not a component) within the fragment.
But your "not working" example just does:
<Rand />
The element is created, but not used, and your function isn't called. The "your function isn't called" part may be surprising — it was to me when I started using React — but it's true:
const { Fragment } = React;
function Rand() {
console.log("Rand called");
return <Fragment>{Math.random()}</Fragment>;
}
console.log("before");
const element = <Rand />;
console.log("after");
// Wait a moment before *using* it
setTimeout(() => {
ReactDOM.render(
element,
document.getElementById("root")
);
}, 1000);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
How can I prevent in both cases to re-render/re-initialize the component?
If you do what you've done in your example, which is to take the mounted component out of the tree entirely, you're unmounting the component instance; when you put it back, your function will get called again, so you'll get a new value. (This is also why the version with the counter doesn't exhibit this behavior: the component instance remained mounted.)
If you want to memoize what it shows, one approach is to pass that to it as a prop, and memoize what you pass it:
const { useState, useMemo, Fragment } = React;
function Rand({text}) {
return <Fragment>{text}</Fragment>;
}
const App = () => {
const [show, setShow] = useState(true);
const working = useMemo(() => <Fragment>{Math.random()}</Fragment>, []);
const randText = useMemo(() => String(Math.random()), []);
return(
<Fragment>
<button
onClick={() => {
setShow(!show);
}}>
{show?"Hide":"Show"}
</button>
<br />
Working:
{show && working}
<br />
Also Working Now:
{show && <Rand text={randText} />}
</Fragment>
);
}
ReactDOM.render(
<App />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

handle useReducer if pass from props

I'm trying to make an overridable usage of a useReducer in a Component for specific use cases.
Following my overfly simplified working state of work :
function defaultToggleReducer (state, action) {
console.log('Default toggler used')
return !state
}
function customTogglerReducer (state, action) {
console.log('Custom toggler used')
return !state
}
// Dummy wrap to log the initialization from `Togller` component to point the twice initializations
function toggleInitializer (state) {
console.log('toggleInitializer')
return state
}
function Toggler (props) {
const [
toggleState,
toggleDispatch,
] = React.useReducer(defaultToggleReducer, false, toggleInitializer)
// Here is the prt making the previous `useReducer` useless
const state = props.toggleState !== undefined ? props.toggleState : toggleState
const dispatch = props.toggleDispatch || toggleDispatch
return (
<button
type="button"
onClick={() => dispatch({ type: 'add' })}>
{Boolean(state).toString()}
</button>
)
}
function App () {
const [customToggleState, customToggleDispatch] = React.useReducer(
customTogglerReducer,
false
)
return (
<div>
<fieldset>
<legend>Default</legend>
<Toggler />
</fieldset>
<fieldset>
<legend>Customized</legend>
<Toggler
toggleState={customToggleState}
toggleDispatch={customToggleDispatch} />
<button
type="button"
onClick={() => customToggleDispatch({ type: 'parentAction' })}>
This is why I want dispatch
</button>
</fieldset>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
<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="root" />
Here, there is something smell bad, I have no doubts about this is working but the fact where useReducer is called in Toggler even if not used let me suspicious. So, it's a dead reducer I think.
So my question is:
Did you have a way to achieve a reduce control from parent component properly (at least: better than this) ?
Use components' composition to solve this problem. Break Toggler to two components - dumb Toggler that expects an outside reducer, and DefaultToggler that renders Toggler, and supplies the reducer.
If you need a standard toggler, use DefaultToggler, and if you need a custom version use the dumb Toggler, and supply a custom reducer.
function defaultToggleReducer (state, action) {
console.log('Default toggler used')
return !state
}
function customTogglerReducer (state, action) {
console.log('Custom toggler used')
return !state
}
// Dummy wrap to log the initialization from `Togller` component to point the twice initializations
function toggleInitializer (state) {
console.log('toggleInitializer')
return state
}
function Toggler ({ toggleState: state, toggleDispatch: dispatch }) {
return (
<button
type="button"
onClick={() => dispatch({ type: 'add' })}>
{Boolean(state).toString()}
</button>
)
}
function DefaultToggler () {
const [
toggleState,
toggleDispatch,
] = React.useReducer(defaultToggleReducer, false, toggleInitializer)
return (
<Toggler
toggleState={toggleState}
toggleDispatch={toggleDispatch}
/>
);
};
function App () {
const [customToggleState, customToggleDispatch] = React.useReducer(
customTogglerReducer,
false
)
return (
<div>
<fieldset>
<legend>Default</legend>
<DefaultToggler />
</fieldset>
<fieldset>
<legend>Customized</legend>
<Toggler
toggleState={customToggleState}
toggleDispatch={customToggleDispatch} />
<button
type="button"
onClick={() => customToggleDispatch({ type: 'parentAction' })}>
This is why I want dispatch
</button>
</fieldset>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
<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="root" />

How to check if long list got rendered in React

Im trying to display a very long list from .json file (2k+ nodes with multiple lines of text). Is there a way to set useState variable after list finishes rendering itself cause useEffect refused to work
import React from 'react';
import LongList from './LongList.json';
const LongList = () => {
const [isLoaded,setIsLoaded] = React.useState(false);
React.useEffect(() => {
setIsLoaded(true);
}, [setIsLoaded]);
return (
<div>
{LongList.map(element => (
<div key={element.text}>{element.text}</div>
))}
</div>
);
};
You can do something like that by checking the index of the current item:
{LongList.map((element, index) => (
<div key={element.text}>{element.text}</div>
if(index === LongList.length - 1) {
// it is loaded
}
))}
You're on the right track with useEffect. I believe part of the issue you're having is due to using setIsLoaded as the second argument to useEffect. Instead, use [], which tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. More info in the React docs.
Here's an example, with a console log in the useEffect callback showing it's only run once.
const data = Array.from(Array(10001).keys());
const LongList = ({data}) => {
const containerRef = React.useRef(null);
const [height, setHeight] = React.useState(0);
React.useEffect(() => {
console.log('Height: ', containerRef.current.clientHeight);
setHeight(containerRef.current.clientHeight);
}, []);
return (
<div>
<div>Height: {height}</div>
<div ref={containerRef}>
{data.map(element => (
<div key={element}>{element}</div>
))}
</div>
</div>
);
};
ReactDOM.render(<LongList data={data} />, document.getElementById('app'));
<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="app"></div>

Resources