A Complete Introduction to React Hooks

Subscribe to my newsletter and never miss my upcoming articles

React hooks are great additions to React which has completely changed how we write code. Hooks were introduced in React starting with version 16.8.0.

Before React hooks, there was no way of using state and lifecycle methods in functional components, and that’s why the functional components were called Stateless Functional Components.

Now, with the introduction of React hooks, we can use life cycle methods and state in functional components. Using hooks makes our code a lot simpler, shorter, and easy to understand.

Installation

To start using React hooks, you need to use the latest version of create-react-app.

If you are using your own webpack setup for React as described in this article, you need to install the latest version of react and react-dom using:

npm install react@latest react-dom@latest

Let’s start exploring React Hooks now.

Hooks are the functions that let you “hook into” the React state and lifecycle features in functional components.

React provides many built-in hooks and also allows us to create our own custom hooks.

Let’s look into some of the most important built-in hooks.

useState Hook

This is the first hook provided by React which allows us to use state inside the functional component.

The useState hook accepts a parameter which is the initial value of the state.

In class-based components, state is always an object but when using useState, you can provide any value as the initial value like number, string, boolean, object, array, null etc.

The useState hook returns an array whose first value is the current value of the state and the second value is the function which we will use to update the state similar to setState method.

Let’s take an example of class-based component which uses state and we will convert it into a functional component using hooks.

Here's a Code Sandbox Demo.

Let’s convert the above code to use hooks.

Here's a Code Sandbox Demo.

Let’s understand the above code

  1. To use the useState hook, we need to import it as we have done it in the first line.
  2. Inside the App component, we are calling useState by passing 0 as the initial value and using destructuring syntax, we stored the array values returned by useState into counter and setCounter variables.
  3. It’s a common convention to prefix the function name used to update the state with set keyword as in setCounter.
  4. When we click the increment button, we are defining an inline function and calling the setCounter function by passing the updated counter value.
  5. Note that, as we already have the counter value, we have used that to increment the counter using setCounter(counter + 1)
  6. Since there is a single statement in the inline on click handler, there is no need for moving the code into a separate function. Though you can do that if the code inside the handler becomes complex.

We can have multiple useState calls in a single functional component:

Here's a Code Sandbox Demo.

As you can see in the above code, we have declared two useState calls and both username and counter are independent of each other.

We can add as many useState calls as needed.

State is not merged when using useState hook

One thing to note if you are using object in useState is that, state is not merged when updating the state.

Let’s see what happens when we combine the username and counter into an object to update the state.

Here's a Code Sandbox Demo.

If you run the code and try to increment the counter, it works but as soon as you try to type into the input field, the counter value vanishes and when you again try to increment the counter, it shows as NaN and you will also get a warning in the console.

This is because using an object in useState will completely replace the object instead of merging it when we set the state using setState method.

In the above code, initial state value is

{ counter: 0, username: "" }

but when we call increment method, it becomes

{ counter: 1 }

and so the username is lost.

If you type in the input field first, then

{ counter: 0, username: "" }

becomes

{ username: "new_value" }

and so the counter is lost.

Because of this, it’s recommended to use multiple useState calls instead of using a single useState to store object.

If you still want to use an object in useState, you need to manually merge the previous state like this:

const handleOnClick = () => { 
  setState(prevState => ({
   ...prevState,
   counter: prevState.counter + 1
  }));
};

const handleOnChange = event => {
 const value = event.target.value;
 setState(prevState => ({
   ...prevState,
   username: value
 }));
};

Here's a Code Sandbox Demo.

useEffect Hook

React provides a useEffect hook using which we can implement lifecycle methods in functional components.

The useEffect hook accepts a function as its first parameter and an optional array as the second argument.

useEffect lets you perform side effects in a functional component.

Data fetching, setting up a subscription, and manually changing the DOM in React components are all examples of side effects.

useEffect takes two arguments: a function and an optional array.

useEffect serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount combined together.

Take a look at the below code:

Here's a Code Sandbox Demo.

If you run the code, you will see that function provided in useEffect is invoked immediately on page load i.e when the component is mounted. It will also be called when the component is re-rendered when any state or prop changes.

So when you click the increment button, the useEffect hook will be called again.

So it serves the purpose of componentDidMount and componentDidUpdate lifecycle methods.

If you want the effect to be called only when the component is mounted, you need to provide an empty array as the second argument to useEffect call.

useEffect(() => {
  console.log("useEffect called");
}, []);

So now, this effect will be called only when the component is mounted and never again.

Here's a Code Sandbox Demo.

Take a look at the below code:

Here's a Code Sandbox Demo.

As you can see, the useEffect gets executed when the counter is changed and also when the username is changed.

If you want that to be executed only when the counter is changed, you need to mention that in the dependencies array which is the second argument to useEffect.

useEffect(() => {
 console.log("This will be executed when only counter is changed");
}, [counter]);

This is very important to understand because if you are doing some computation in useEffect when the counter is changed, there is no point in executing that code when the username or any other prop or state is changed.

So by providing the variable in the dependencies array, the effect will be executed only when those dependencies are changed.

This is a great improvement because it will remove the need for the extra condition we used to have in the componentDidUpdate method

componentDidUpdate(prevProps) {
 if(prevProps.counter !== this.props.counter) {
  // do something
 }
}

So using useEffect and mentioning dependencies removes the need of comparing previous props or state value with the current prop or state value because the code inside useEffect will only be executed when the dependencies change.

If you have multiple dependencies, you can specify that as a comma-separated value in the dependencies array.

useEffect(() => {
 console.log("This will be executed when any of the dependency is changed");
}, [counter, some_other_value]);

Cleaning up the effect

If you return a function from the useEffect, it will be called before the component will be unmounted. So it’s similar to componentWillUnmount method.

Let’s say you are using the setInterval function, then you can clear the interval in the function returned by useEffect.

This cleanup effect runs when the component unmounts as well as before re-running the useEffect due to a subsequent render.

Here's a Code Sandbox Demo.

In the above code, we have added a scroll event handler which will be registered only once, when the component is mounted because we have passed an empty array [] in useEffect dependencies list.

This code displays a message in the console when the page is scrolled.

When we click on the unmount component button, the component will be removed and the function returned from the useEffect will be called where we remove the scroll event handler and print the message component unmounted to the console.

It is important to note that in the child component, useEffect will be called for every re-render of the parent component.

Take a look at the below code:

Here's a complete Code Sandbox demo.

Here, we are adding a new todo. So for every added todo, the TodoList component will be re-rendered and so the useEffect defined in the TodoList will be called for every re-render printing the message in the console.

So if you don’t want to re-render the effect of TodoList, you need to pass an empty array [] as the dependency in the useEffect second argument.

useEffect(() => {
 console.log("This will be executed only once");
}, []);

Multiple useEffect in a single component

You can also have multiple useEffect calls in a single functional component, each doing different tasks, and React will call them in the order they are defined in the component.

useRef Hook

The useRef hook allows us to get access to any DOM element which is the same as working with ref in React.

It has the following syntax:

const refContainer = useRef(initialValue);

The initialValue is optional.

Here's a Code Sandbox Demo.

As you can see we have created the reference using:

const usernameRef = useRef();

and assigned it to the input field. So now we can access the input field by using usernameRef.current property anytime we want to get access to the input field.

useRef is not exactly the same as React ref because useRef returns a mutable ref object whose current property is initialized to the passed argument (initialValue).

The returned object will persist for the full lifetime of the component.

Here's a Code Sandbox Demo.

In the above code, we have created a timer that displays the updated time after every second. When we click the unmount component button, then we are clearing the setInterval thereby stopping the timer.

Also, note that we have added another useEffect call which clears the timer after 5 seconds are passed.

The key point to remember is that we are able to access the same value of the interval variable in the first useEffect, even when it’s assigned in the second useEffect.

If we didn’t use useRef but used the normal variable, it will not be available in the first useEffect because useEffect creates a new function for every execution of the effect.

useReducer Hook

React provides a useReducer hook which allows us to use redux for handling complex calculations without the need of installing the redux library.

So it helps to avoid the creation of boilerplate redux action, reducer code.

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.

The useReducer has the following syntax:

const [state, dispatch] = useReducer(reducer, initialState);

The useReducer accepts reducer and initialState and returns the current state and a dispatch function which we can use to dispatch the actions.

Let’s understand through an example. We will create a todo list component that will allow us to add and remove a todo.

Let’s create a reducer to handle the todo.

const todosReducer = (state, action) => {
 switch (action.type) {
  case "ADD_TODO":
   return [...state, action.todo];
  case "REMOVE_TODO":
   return state.filter(todo => todo !== action.todo);
  default:
   return state;
 }
};

We initialize the initial state of todosReducer in initialState of useReducer as:

const initialState = [];
const [state, dispatch] = useReducer(todosReducer, initialState);

and dispatch the actions like

dispatch({ type: "ADD_TODO", todo: value });

Here's a Code Sandbox Demo.

As you can see, the useReducer hook makes it very easy to manage the redux state without actually using redux which is a great improvement.

But this does not mean redux is useless now. If you are managing the larger state or have multiple reducers, you should use redux to handle that.

useReducer just make it easy if you want to handle complex states in a single component.

Creating Custom Hooks

React allows us to create our own custom hooks using the built-in hooks. This allows us to extract component logic into reusable functions.

Let’s take the previous timer code from here and we will convert it into a custom reusable hook.

Here's a Code Sandbox Demo.

Here, we have encapsulated the code for displaying a timer in the useTimer function and from that function we are returning the updated time.

So now any component that wants to display the time, can call the useTimer function and use this timer hook.

It’s a common convention to begin the custom hook name with use keyword like useTimer.

Things to keep in mind when working with Hooks

  • Only call Hooks at the top level. Don’t call Hooks inside loops, conditions, or nested functions. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.

  • Only call Hooks from React function components. Don’t call Hooks from regular JavaScript functions. But you can call it from your own custom Hooks.

To learn how to handle API calls using Async/await with the useEffect hook check out this article.

Thanks for reading!

Want to learn all ES6+ features in detail including let and const, promises, various promise methods, array and object destructuring, arrow functions, async/await, import and export and a whole lot more from scratch?

Check out my Mastering Modern JavaScript book. This book covers all the pre-requisites for learning React and helps you to become better at JavaScript and React.

Check out free preview contents of the book here.

Also, you can check out my free Introduction to React Router course to learn React Router from scratch.

Want to stay up to date with regular content regarding JavaScript, React, Node.js? Follow me on LinkedIn.

Interested in reading more such articles from Yogesh Chavan?

Support the author by donating an amount of your choice.

Recent sponsors

Comments (2)

Fernando dos Santos's photo

Awesome!