Hacker News new | ask | show | jobs
by alayek 2377 days ago
I'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

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

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