News for hackers. Stuff that matters.
Re:Thinking in React Hooks by steve_taylor on Thursday December 12, 2019 @08:53PM (#21778482)
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.

Re:Thinking in React Hooks by foolbiter on Friday December 13, 2019 @12:31AM (#21779345)

  function Counter() {
    const [counter, setCounter] = React.useState(0);
    React.useEffect(() => {
      const interval = setInterval(() => setCounter(n => n + 1), 100);
      return () => clearInterval(interval);
    }, [])

    return (
      <div>{counter}</div>
    );
  }
Re:Thinking in React Hooks by steve_taylor on Saturday December 14, 2019 @03:55AM (#21788530)
I had no idea the setter can take a function instead of a value. It makes perfect sense, because setState has the same feature. TIL.
Re:Thinking in React Hooks by acemarke on Saturday December 14, 2019 @08:56PM (#21793935)
FWIW, it's documented in the API reference:

https://reactjs.org/docs/hooks-reference.html#functional-upd...

Re:Thinking in React Hooks by andrewstuart on Friday December 13, 2019 @12:39AM (#21779369)
Does this continuously clearInterval on every render?
Re:Thinking in React Hooks by foolbiter on Friday December 13, 2019 @01:45AM (#21779624)
Nope. 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.
Re:Thinking in React Hooks by andrewstuart on Friday December 13, 2019 @03:52AM (#21780108)
I like it. How did I miss this most obvious and simple solution?
Re:Thinking in React Hooks by andrewstuart on Thursday December 12, 2019 @10:08PM (#21778836)
This 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};
Re:Thinking in React Hooks by steve_taylor on Thursday December 12, 2019 @10:56PM (#21779038)
But 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.

Re:Thinking in React Hooks by andrewstuart on Friday December 13, 2019 @12:29AM (#21779339)
Ah 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.

Re:Thinking in React Hooks by gear54rus on Thursday December 12, 2019 @11:47PM (#21779197)
Re:Thinking in React Hooks by deviantfero on Thursday December 12, 2019 @11:42PM (#21779188)
Class components are not going anywhere, so if hooks don’t fit your use case, choose the better tool to do it
Re:Thinking in React Hooks by alayek on Friday December 13, 2019 @12:22AM (#21779319)
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