Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use React hook to implement a self-increment counter [duplicate]

The code is here: https://codesandbox.io/s/nw4jym4n0

export default ({ name }: Props) => {   const [counter, setCounter] = useState(0);    useEffect(() => {     const interval = setInterval(() => {       setCounter(counter + 1);     }, 1000);      return () => {       clearInterval(interval);     };   });    return <h1>{counter}</h1>; }; 

The problem is each setCounter trigger re-rendering so the interval got reset and re-created. This might looks fine since the state(counter) keeps incrementing, however it could freeze when combining with other hooks.

What's the correct way to do this? In class component it's simple with a instance variable holding the interval.

like image 405
PeiSong Avatar asked Nov 20 '18 14:11

PeiSong


People also ask

Can I use useEffect twice in React?

If you have just made a new project using Create React App or updated to React version 18, you will notice that the useEffect hook is called twice in development mode. This is the case whether you used Create React App or upgraded to React version 18.

Can I use useEffect twice in same component?

twice. Specifically, this component is mounted, then unmounted, and then remounted. This also means that when you fetch data in useEffect, it will be sent twice!

Why is useEffect rendering twice?

The useEffect callback runs twice for initial render, probably because the component renders twice. After state change the component renders twice but the effect runs once.

Are React hooks memoized?

The React useMemo Hook returns a memoized value. Think of memoization as caching a value so that it does not need to be recalculated. The useMemo Hook only runs when one of its dependencies update.


1 Answers

You could give an empty array as second argument to useEffect so that the function is only run once after the initial render. Because of how closures work, this will make the counter variable always reference the initial value. You could then use the function version of setCounter instead to always get the correct value.

Example

const { useState, useEffect } = React;  function App() {   const [counter, setCounter] = useState(0);    useEffect(() => {     const interval = setInterval(() => {       setCounter(counter => counter + 1);     }, 1000);      return () => {       clearInterval(interval);     };   }, []);    return <h1>{counter}</h1>; };  ReactDOM.render(   <App />,   document.getElementById('root') );
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script> <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>  <div id="root"></div>

A more versatile approach would be to create a new custom hook that stores the function in a ref and only creates a new interval if the delay should change, like Dan Abramov does in his great blog post "Making setInterval Declarative with React Hooks".

Example

const { useState, useEffect, useRef } = React;  function useInterval(callback, delay) {   const savedCallback = useRef();    // Remember the latest callback.   useEffect(() => {     savedCallback.current = callback;   }, [callback]);    // Set up the interval.   useEffect(() => {     let id = setInterval(() => {       savedCallback.current();     }, delay);     return () => clearInterval(id);   }, [delay]); }  function App() {   const [counter, setCounter] = useState(0);    useInterval(() => {     setCounter(counter + 1);   }, 1000);    return <h1>{counter}</h1>; };  ReactDOM.render(   <App />,   document.getElementById('root') );
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script> <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>  <div id="root"></div>
like image 61
Tholle Avatar answered Oct 04 '22 22:10

Tholle