DEV Community

Thomas Pikauli
Thomas Pikauli

Posted on

I used effects, said goodbye to my lifecycles, and feel 🙂

Last week Hooks were finally added to React. It is now possible to use state in a function. At least, that much I remembered from watching the initial announcement way back when. But lifecycles though... I had no idea how they were supposed to work. I decided to read the docs and experiment.

Let's experiment

Let's try and build a little app that keeps a few bits of state and does things based on state changes. Normally I would look to my beloved componentDidUpdate for this, but she's gone... well not really: classes will very much remain part of React, but still... I might have already said goodbye to them.

Okay, the 'app'. Let's create a numerical input field with an onChange handler, a button to add said input to whatever the input was before, and finally a button to switch from dark to light mode and vice-versa (somehow everyone seems to love that).

The initial state for that in our Hookian Age would look a little something like this:

function App() {
  const [lights, setLights] = useState(true);
  const [input, setInput] = useState(0);
  const [sum, setSum] = useState(0);
}

Next up we're going to hook it up to HTML elements, just like we'd do in our classes.

return (
    <div
      style={{
        padding: 15,
        fontFamily: "Arial",
        background: lights ? "salmon" : "black",
        color: lights ? "black" : "white"
      }}
    >
      <h1
        style={{ fontFamily: "Times New Roman", margin: 0, paddingBottom: 15 }}
      >
        Sumple UseEffect
      </h1>
      <div>
        <strong>Sum:</strong> {sum}
      </div>
      <div style={{ marginTop: 15 }}>
        <input
          type="number"
          value={input}
          onChange={e => setInput(e.target.value)}
          style={{ width: 50 }}
        />
        <button onClick={handleSum}>
          +
        </button>
      </div>
      <div style={{ position: "absolute", top: 30, right: 30 }}>
        <button onClick={() => setLights(!lights)}>
          Lights {lights ? "off" : "on"}
        </button>
      </div>
    </div>
  );

Hopefully this doesn't look weird to you. The only thing that's missing from the code is the handleSum function. It looks like this:

  function handleSum() {
    setSum(Number(sum) + Number(input));
    setInput(0);
  }

It's defined within our App function and simply adds the input state to our sum state; and then sets the input back to zero. We should have a working app now. So far, so good.

We Use Effects

Now we've decided we want to validate the numerical input. If the value is 0 or above a very arbitrarily chosen number of 480, we're going to disable the + button. Granted, we don't actually need an extra piece of state for this, but let's just do it anyway.

We create valid as a piece of state.

  const [valid, setValid] = useState(true);

and then we add disabled to our button.

  <button disabled={!valid} onClick={handleSum}>
     +
  </button>

If we were still using classes we could have done something like this.

componentDidUpdate(prevProps, prevState) {
  if(prevState.input !== this.state.input) {
     if (this.state.input > 480 || this.state.input === 0) {
        this.setState({valid: false});
      } else {
        this.setState({valid: true});
      }
  }
}

But we're not. So we don't have componentDidUpdate. Instead we do have useEffect, a function that React promises to run after each render. That's convenient, because each change in input triggers a render. So we can use useEffect to do our validation.

  useEffect(
    () => {
      if (input > 480 || input === 0) {
        setValid(false);
      } else {
        setValid(true);
      }
    },
    [input]
  );

Our new friend accepts a function as a first argument, and an array as an optional second argument. I hope that the function makes sense, but I would understand if you have questions about the lack of comparing state and the array. Luckily, those two are connected.

It turns out, you don't have to compare props or state anymore. React allows you to provide the values in the second argument you want to run useEffect on. So in our case useEffect will only run on changes in input.

Did we mount?

So we now have a way to 'do stuff' based on changes in props or state. But how about that initial lifecycle that used to help us set up subscriptions? Or that one that allowed us to clean them up again? I'm talking about componentDidMount and componentWillUnMount of course.

We need a way to run useEffect regardless of changes, but not on every change. Preferably only once in the beginning, and once at the end. Luckily, that's very much possible.

  function handleEscape(e) {
    if (e.keyCode === 27) {
      setSum(0);
      setInput(0);
    }
  }

  useEffect(() => {
    document.addEventListener("keydown", handleEscape);
    return () => document.removeEventListener("keydown", handleEscape);
  }, []);

If we leave the array empty in our second argument, React runs our code only once, and treats our return as a cleanup function for when our component unmounts. In our case we can use it to add a listener to reset our sum and input when someone presses escape.

But what about our other useEffect? Do we have to merge them...?

The answer is NO! You can use multiple useEffects within the same component. Which is great, because we're going to add even more. I'm very curious to see how many times I change the lights or sum.

So we set up another useEffect and fill our array with lights and sum. Now this effect will only run when either lights or sum changes. It's good, but I just realized I want to know which of the two changed so I can keep more detailed stats.

The answer is actually to make two useEffects, but I dream back to the days (yesterday) when I still used classes and want to do a comparison between previous and current values, so I find out myself which one changed. How to get those previous values though? The React docs provide us with a helping hook.

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

It's used like this:

  const previousValues = usePrevious({ lights, sum });

So what's happening is that we pass our state values into the function, they are then saved in a ref and that ref is returned. This ensures we do not get the latest values, but the previous values. That might seem much more difficult then it needs to be, but remember that we could have just used two useEffect functions each with the other value in the second argument.

Let's complete our single useEffect though:

  useEffect(
    () => {
      if (previousValues && previousValues.lights !== lights) {
        const newActions = Object.assign({}, actions, {
          lights: actions.lights + 1,
          total: actions.total + 1
        });
        setAction(newActions);
      } else if (previousValues && previousValues.sum !== sum) {
        const newActions = Object.assign({}, actions, {
          sum: actions.sum + 1,
          total: actions.total + 1
        });
        setAction(newActions);
      }
    },
    [lights, sum]
  );

That works. We now have a log of which actions have been used. Let's render it on the page.

  <div>
    <strong>Actions:</strong> {actions.total}
  </div>

Have we exhausted our useEffect trickery? We have not. How about one that runs on each render? That would allow us to count how many renders we see right? Could be interesting. Let's do it.

But wait. Even if we have a useEffect that runs after each render, how will we save our amount of renders without triggering another render? We can useRef for that.

  const renders = useRef(0);

We initialize the ref with 0. Then we create another effect.

  useEffect(() => {
    renders.current = renders.current + 1;
  });

Since the current property of a ref is mutable, we can assign our new value to it. But did you notice how our useEffect doesn't have a second argument? I think I mentioned it before, but the second argument is optional. If you do not provide it, the useEffect runs on each render. Perfect for this case.

All that's left is to render our render count on the screen.

  <div>
    <strong>Renders:</strong> {renders.current}
  </div>

Yes, you can just render the current value of the ref. Awesome!

Am I happy?

After writing this app I wonder if I'm happy with all of this. To be honest, I think I am. While useEffect takes a bit of getting used to, it's very versatile. That might also be its 'problem'. Although, once you really start using useEffect you may not find that a problem at all.

It probably will take a bit more time before I get really comfortable with Hooks and all that they bring with them, but I already feel their potential. If you have the space and time to experiment with them, I strongly recommend to do so. There's no hurry, but they're cool. They really are 🙂.

Full code if you are interested.

Top comments (5)

Collapse
 
marklai1998 profile image
Mark Lai

Am I the only person think useEffect is cool but hard to read?

people who new to Reactjs will really confuse with the setup
since "useEffect" doesn't tell anything on its name but "componentDidUpdate" definitely tells when the function will be called
Also, the unmount function part is hard to read

I think it should replace HOC, but not for middle size component(like your example)

Collapse
 
marklai1998 profile image
Mark Lai • Edited

functional component is not as organized as a class component
Giving the power of state is great, but ppl(lots of programmers) usually abuse it

what if your component has react.memo + useEffect + useState inside and outside a function? really a pain in the ass to a new react programmer.
Will definitely lower the intention of ppl learning react

Collapse
 
ma5ly profile image
Thomas Pikauli

I kind of agree with you Mark. I think hooks are really powerful, which makes them easy to abuse. An 'experienced' React programmer can make components unnecessarily complex just to be 'smart'.

And like you say, that would be a huge pain for beginners. The class structure is much easier to read in that sense. But having used React for a bit longer now, I do appreciate what hooks allow you to do. You really feel more in charge of your components and logic.

I guess with great power comes great responsibility :)

Collapse
 
kayis profile image
K

Yes, the naming isn't optimal, I think.

useState is pretty straight-forward, but useEffect not so much.

On the other hand, they are basic building blocks used to create more sophisticated hooks that would have clearer names.

useFetch, useHttp, useTrackingApi, useFacebookLogin etc.

Collapse
 
ma5ly profile image
Thomas Pikauli

Hey Radu! Thanks for the feedback. To be honest, I was quite unaware of this, but thinking about it, it actually makes sense :)

I've updated the code! Thanks again :)