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)
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)
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
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 :)
Yes, the naming isn't optimal, I think.
useState
is pretty straight-forward, butuseEffect
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.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 :)