Hey everyone, on the aftermath of the workshop Hooked on Hooks — Playing around with React Hooks, I’ve decided to share a bit of it with you. This article focuses on the motivation that led to creating Hooks following a brief introduction to each of them. This article ends with the rules of Hooks.
A significant part of this article is adapted from the docs, and it doesn’t replace reading them and watching “React Today and Tomorrow, and 90% Cleaner React With Hooks”.
A life without Hooks
Let’s go back to a life without Hooks. The main pain points focused on three things:
- Giant and confusing components
- Reusing logic between components
- Confusing classes
Giant and confusing components
Looking at the example above, what do we have?
Three lifecycle components (componentDidMount, componentDidUpdate, and componentWillUnmount) each one of them with blocks of logic that is unrelated with the rest on that block. As it is right now, the code is readable, and there isn’t much logic split.
Now let us consider that you add more code inside one of those lifecycle functions. This addiction leads to more and more unrelated logic coupled inside the same function and split from the related code. The code grows to be more unreadable, and lets not even talk about how are we going to test this.
Reusing logic between components
If you were to think, what does React offers natively so that I can reuse stateful logic between components? Does anything ring a bell?
If I were asked this question a couple of weeks ago, I’d probably say mixins (if I was an old school React developer 😄) or patterns like render props or high order components.
The thing is, mixins have been considered harmful, and patterns like HOCs and render props require code restructuring, adds extra complexity, leads to unreadable code, and can cause something that the React team refers to as “wrapper hell.”
This one may cause a bit of controversy, but classes can be confusing.
Excluding the human part, classes may be confusing for machines as well as they don’t minify properly, cause issues on hot reloading, and overall may introduce some erratic patterns.
The three pains above were described as a symptom of a bigger problem. That is, React doesn’t provide a stateful primitive simpler than a class component.
To fix this issue, the React team introduced Hooks.
Now, why are they called Hooks? What are they? What do I win by using them?
Hooks are functions exposed by React that you can use from inside your functional components to access some of its features — like state for instances.
Or, if you want to explain it on a one-liner:
Hooks lets you hook into React features from function components
So with Hooks, React now has a new stateful primitive that improves code reusability without changing the component hierarchy, by being able to create custom Hooks. Let’s split your components into smaller ones where the related logic is all coupled together, improving code readability, and use other React features without classes. All this without causing breaking changes!
After that summary, let’s do a before Hooks and after Hooks with the basic Hooks.
As you can see above, we have a “simple”
Countercomponent. Well, “simple” given the amount of boilerplate, we need to increment the counter — this passes by having this all around, binding functions, and all of it leads to some lines of code increase.
The first thing we see is that we removed unnecessary boilerplate: no constructor, no unnecessary binds, and overall half the number of lines of code.
Now what we do is, inside our function, we tap into the React useState Hook. We pass it an initial value, and it returns us the state variable and a function we can use to update the state. Pretty simple right?
- When well call the function to update the state, it enqueues a re-render of the component.
- You can have multiple state variables with useState
- Contrary to the previous setState, useState replaces state objects instead of merging them.
- If you are using an object as state, when you are setting state, you can spread the state to avoid losing properties.
The example above fits precisely with the example on the Giant and confusing components section. So let’s avoid repeating text and go forward to learn about useEffect.
Passing from class components to functions, the first thing we get rid of is lifecycles. So, if we don’t have lifecycles, how can I run stuff depending on changes to state, or on the component mount for instances?
Given this, React introduced useEffect. useEffect lets us run side effects on our function components. So it’s a shift from thinking in lifecycles to thinking in effects. It may seem a bit confusing explained like this, but we’ll understand it going forward.
Look at this syntax above. What we see here is that the useEffect accepts two arguments being the first a function to be run as an effect and the second one an array of values, which is called dependencies array. Think of the dependencies array as a group of variables that React is looking at, and when one of them changes, React will say: “Hey, on your dependencies has changed. Run this effect, please”.
With this knowledge under our belt, let’s look at the first useEffect block.
What happens on that first effect is, every time our title variable changes, then React will run an effect responsible for changing the document title. With what is a simple effect we just covered two lifecycles and reduced a great amount of code.
Let’s look at the second effect as it is a bit different from the other one. The first thing we may see is the dependencies array is empty — that means that that Effect will only be run once given that it doesn’t have any dependencies to watch. The second thing is that return, this return is what is called a cleanup, and will be run when our effect is being cleaned up (you can compare it to componentWillUnmount). So what we are doing on this effect is, when our component is first rendered, we will subscribe to a store, and when it’s time to clean up that effect, then we will unsubscribe from that store.
With useEffect, you can couple related logic, making the code more readable and more comfortable to debug and do several effects according to our needs.
- If no dependencies array is passed the effect will run every re-render
- If an empty dependencies array is passed we aren’t watching any changes so the effect will only run on the first render.
- You can return a function from our effect that will be responsible for cleaning it up.
- Be careful with objects inside the dependencies array as if they aren’t memoized then the references will change on every re-render forcing that effect to run unexpectedly.
React team created context to have a way to pass data between a component tree without having to prop drill through every level. This solution passed by declaring a context provider at a top-level and a context consumer on the components that needed to consume that context. The next example shows how we could consume a context.
It may seem pretty simple, right? But let’s imagine that on the same component, we need to consume more than one context. We will end-up having our render tree wrapped with multiple consumers. This leads to the above mentioned “wrapper hell.”
With the introduction of useContext now, you can access any context data at our component top level and use those props freely around our code. There are no more “wrapper hell,” and our render is readable and only consists of the presentational part of our component.
- A component that is subscribing to changes in context re-renders when any of the context values change.
Now that we reviewed the basic Hooks, let’s delve into some additional Hooks.
Why additional Hooks, you ask. That is because they either variate from the basic ones or are just needed on some edge cases. Here we do a brief description of each.
( If you have used Redux you may be already familiar with this concept)
useReducer is a useState alternative that accepts a reducer and an initial state and returns the current state and a dispatch. The following example comes straight from the React docs and perfectly illustrates how to work with useReducer.
Allows you to memoize expensive functions so that you can avoid calling them on every render
Since useMemo tends to be a bit confusing, I’m going to try to clarify it. Let’s look at the following example.
Let’s consider we have an expensive function
computeLetterCount. If we just did a simple assignment of its output to the
letterCountvariable, then every render of our component that function would recompute. This recomputation would lead to a slowing down of our component. Entering useMemo to the rescue — by wrapping our function call with useMemo and passing it a dependencies array (remember useEffect?), then React makes sure that the function call only recomputes when a dependency changes. So, now, our expensive computation only happens when that word changes and avoids slowing down our component on every render.
It allows you to memoize functions so that you can avoid creating them on every render.
useCallback is another Hook that is more simple to understand if we go step-by-step. Let’s imagine we have a component that uses React.memo to guarantee that the component only re-renders when the props change.
If we create a function, every time that component renders, then it creates a new reference for it. What will this cause?
If we are passing that function to our component that is wrapped by React.memo, then every re-render that function is considered a new one, leading to unnecessary re-renders happening on that component.
useCallback guarantees that while any variable on its dependencies array (remember useEffect again?) doesn’t change, then React gives you precisely the same reference for that function allowing for React.memo to detect that there weren’t any changes and not re-rendering that component.
Rules of Hooks
Ony call Hooks at the top level
Hooks shouldn’t be called from inside conditions, loops, or nested functions. That is because React relies on the order in which the Hooks are called, and if you skip a Hook update, then that order would be changed and cause erratic behavior.
Only call Hooks from React functions
This one is straightforward — Hooks may only be called from React function components or custom Hooks.
With this article, we learned what motivated the appearance of Hooks and why they allow us to write React code the way it always supposed to be — reusable.
Despite being around for a year by now, Hooks are nowhere as close to the expected potential. Many people still fear to learn Hooks and I hope this article gave you all some insights into them and you decide to give them a shot.
I hope you enjoyed and stay tuned because some brand new Hooks are coming (I’m looking at you Concurrent Mode).
Have a great weekend!