An Introduction to React Hooks

An Introduction to React Hooks

React hooks are a great addition 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 second value is the function which we will use to update the state similar to setState method.

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

Demo: https://codesandbox.io/s/delicate-thunder-xdpri

Let’s convert the above code to use hooks.

Demo: https://codesandbox.io/s/elegant-heyrovsky-3qco5

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 intocounter andsetCounter 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:

Demo: https://codesandbox.io/s/long-framework-n1tyn

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.

The state is not merged when using the useState hook:

One thing to note if you are using object in useState is that the 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.

Demo: https://codesandbox.io/s/sleepy-darkness-6jz9b

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 state using setState method.

In the above code, the initial state value is

{ counter: 0, username: "" }

but when we call increment method, it becomes

{ counter: 1 }

and so username is lost.

If you type in the input field first, then

{ counter: 0, username: "" }

becomes

{ username: "new_value" }

and so counter is lost.

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

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
 }));
};

Demo: https://codesandbox.io/s/strange-dawn-s0nyi

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 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:

Demo: https://codesandbox.io/s/stupefied-darwin-qpcmm

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.

Demo: https://codesandbox.io/s/gifted-newton-71vzl

Take a look at the below code:

Demo: https://codesandbox.io/s/long-framework-n1tyn

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 dependencies array, the effect will be executed only when those variables 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 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

Demo: https://codesandbox.io/s/sleepy-wood-biu89

In the above code, we have added 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 prints 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:

Complete code And Demo: https://codesandbox.io/s/jolly-tesla-gj170

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 same as working with ref in React.

It has the following syntax:

const refContainer = useRef(initialValue);

The initialValue is optional.

Demo: https://codesandbox.io/s/damp-water-fye8r

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 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.

Demo: https://codesandbox.io/s/black-cache-e5vlb

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 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 ofuseReducer as

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

and dispatch the actions like this:

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

Complete Code and Demo: https://codesandbox.io/s/admiring-bose-jw6wx

As you can see, the useReducer hook makes it very easy to manage redux state without actually using redux which is 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.

Demo: https://codesandbox.io/s/infallible-visvesvaraya-qkwdw

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 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.

That’s it for today. I hope you learned something new today.

I hope you've been enjoying my articles and tutorials I've been writing. If you found them useful, consider buying me a coffee! I would really appreciate it.

Don’t forget to subscribe to get my weekly newsletter with amazing tips, tricks, and articles directly in your inbox here.

Did you find this article valuable?

Support Yogesh Chavan by becoming a sponsor. Any amount is appreciated!