4 ms·
Try creating a hooks-based component that increments an internal counter every second, using useState to track the counter and setInterval inside useEffect to s
by steve_taylor 7y ago
Try creating a hooks-based component that increments an internal counter every second, using useState to track the counter and setInterval inside useEffect to start the timer on mount and stop it before unmounting.
Good luck with that.
- andrewstuart 7y agoThis should work: import React, {useState, useEffect} from 'react'; const CounterComponent = React.memo(() => { const [counter, setcounter] = useState(0); useEffect( () => { let counterTimer = null; const incrementCounter = () => setcounter(counter + 1); // counterTimer is null if it has not yet been started if (counterTimer === null) { counterTimer = setInterval(incrementCounter, 1000); } // Remove event listener on cleanup - note the remove function signature needs to match the add function sig return () => clearInterval(counterTimer); }, [counter] ); return <span>{counter}<br/></span>; }); export {CounterComponent};
- steve_taylor 7y agoBut you're setting up and tearing down an interval each time counter changes, so it's no better than setTimeout. Compared to a single setInterval, it's going to gradually drift over time. counterTimer is always going to be null where you're checking it for null. You can't really do it properly without also using useRef to track the value. The reason is that the object returned by useRef is always the same, so it's never stale.
- andrewstuart 7y agoAh yes I recall. The key thing to understand about doing this is to set the state in a context, and do the displaying in another component. There's no point in trying to get the component to re-render when the time value updates. That's the wrong way to program react.
- deviantfero 7y agoClass components are not going anywhere, so if hooks don’t fit your use case, choose the better tool to do it
- gear54rus 7y agohttps://overreacted.io/making-setinterval-declarative-with-react-hooks/ https://overreacted.io/making-setinterval-declarative-with-r...
- alayek 7y agoI'm not sure why you believe this would be a difficult task to achieve. You've already mentioned in the other comment, that drift is your concern, and that can be fixed easily. Though it has nothing to do with hooks. One has to track the time when the clock starts, and query current time on every tick. As for creating a setInterval() and tearing it down after every render, you can simply pass an empty deps list, instead of declaraing counter variable as a dep in the array. Or, if you want to keep ESLint happy, declare counter as a dep, but use `setTimeout()` instead of `setInterval` I took a stab at this (React with TS): import * as React from "react"; const TICK_INTERVAL: number = 1000; function App() { const [counter, setCounter] = React.useState<number>(0); React.useEffect(() => { const startTime = Date.now(); const timer = setInterval(() => { setCounter(_ => { const timeNow = Date.now(); return Math.round((timeNow - startTime) / TICK_INTERVAL); }); }, TICK_INTERVAL); return () => { if (timer) { clearInterval(timer); } }; }, []); return ( <div className="App"> <h1>{counter}</h1> </div> ); Codesandbox link with working demo: https://codesandbox.io/s/amazing-solomon-mf3r6 https://codesandbox.io/s/amazing-solomon-mf3r6 But this code wouldn't be that different if you wrote it in class fashion, except you'd be making a few of calls to this. Most of this would go in componentDidMount and the cleanup would go in componentWillUnMount Where hooks really shine, is if you want to add / extend this functionality. Say, you now want the counter to pause when you're not looking at that tab, and resume once the browser tab is in focus. Imagine if you had a pageVisibility API hook, and it returns true and false accordingly, based whether or not the tab is visible at any point of time or not. In a real-world scenario, it'd not be a basic counter, but maybe an API polling for real time data, and user don't want the page to keep on polling when not in focus. In that case, you'd have two changes: one call to the useVisible hook, and one more to pass the boolean output of this hook into your Effect hook. const TICK_INTERVAL: number = 1000; function App() { const [counter, setCounter] = React.useState<number>(0); const startTime = React.useRef<ReturnType<typeof Date.now>>(Date.now()); const isVisible: boolean = useVisibility(); // assume taken from NPM React.useEffect(() => { const timer = setInterval( () => { if (TICK_INTERVAL && isVisible) { setCounter(_ => { const timeNow = Date.now(); return Math.round((timeNow - startTime.current) / TICK_INTERVAL); }); } }, isVisible ? TICK_INTERVAL : null ); return () => { if (timer) { clearInterval(timer); } }; }, [isVisible]); return ( <div className="App"> <h1>{counter}</h1> </div> ); } Working codesandbox demo: https://codesandbox.io/s/heuristic-mcnulty-oo58z https://codesandbox.io/s/heuristic-mcnulty-oo58z Now, let's go one step further, and add local-storage / indexed DB for storing these values, on page close. If you close the page, and re-open, it should resume from where it was before closing, and then count up from there - not zero. All we need now, is another hook, that abstracts away that storage interaction, storing on window.unload and componentWillUnMount, and retrieving value from storage when component mounts for the first time. After this point, it'd be illuminating to look back and try to combine these three functionalities, using class components with HOC or Render Props pattern; and think about the effort it'd take to decouple and reuse the count-up logic, from the page visibility logic, from the storage logic. Edit: formatting
- foolbiter 7y agofunction Counter() { const [counter, setCounter] = React.useState(0); React.useEffect(() => { const interval = setInterval(() => setCounter(n => n + 1), 100); return () => clearInterval(interval); }, []) return ( <div>{counter}</div> ); }
- andrewstuart 7y agoDoes this continuously clearInterval on every render?
- foolbiter 7y agoNope. Since the dep array is empty, the effect only runs once after the first render, & the cleanup function returned from the effect only runs on unmount.
- andrewstuart 7y agoI like it. How did I miss this most obvious and simple solution?
- steve_taylor 7y agoI had no idea the setter can take a function instead of a value. It makes perfect sense, because setState has the same feature. TIL.
- acemarke 7y agoFWIW, it's documented in the API reference: https://reactjs.org/docs/hooks-reference.html#functional-updates https://reactjs.org/docs/hooks-reference.html#functional-upd...