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

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" />
);

Related

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>

react useState not re rendering

I have a pretty simple useEffect hook
const [tagsWithData, setTagsWithData] = useState([]);
useEffect(() => {
....
const finalsTags = temp.map((item) => item.name);
setTagsWithData(finalsTags);
}, []);
Inside of return, I have condition to render the input tag
{tagsWithData.length !== 0 ? (
<TagsInput
selectedTags={selectedTags}
tags={tagsWithData}
/>
) : (
<TagsInput
selectedTags={selectedTags}
tags={tags}
/>
)}
The above code always stays on 0 and it does not move to the else condition.
What am I making wrong here.
Thank you
Your useEffect is not being told to update. useEffect needs to be passed the value/dependencies that it needs to (trigger the) update on. Without it, the effect will only run once on (initial) component render
const [tagsWithData, setTagsWithData] = useState([]);
useEffect(() => {
....
const finalsTags = temp.map((item) => item.name);
setTagsWithData(finalsTags);
}, [temp]); // <--- add this
Below is a small example illustrating the differences. Click on the button, and check out the output of both effectWithDep and effectWithoutDep. You'll notice only effectWithDep will update.
// Get a hook function
const { useState, useEffect } = React;
const Example = ({title}) => {
const [count, setCount] = useState(0);
const [effectWithDep, setEffectWithDep] = useState(0);
const [effectWithoutDep, setEffectWithoutDep] = useState(0);
useEffect(() => {
setEffectWithDep(count)
}, [count])
useEffect(() => {
setEffectWithoutDep(count)
}, [])
return (
<div>
<p>{title}</p>
<p>effectWithDep: {effectWithDep}</p>
<p>effectWithoutDep: {effectWithoutDep}</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
};
// Render it
ReactDOM.render(
<Example title="Example using Hooks:" />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>

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 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