React Hooks made easy with Examples

New to React and trying to get what are hooks? Checkout the easy explanation with examples in this article.

·

14 min read

Introduction

Before diving into the React Hooks, let me give you a brief about what is react.

What is React?

React is a javascript's Frontend library, not a framework. It is developed by Facebook (now Meta) in 2013. React makes Frontend easy to build by giving us the power to divide a page into components. It uses JSX as its templating which is a mixture of HTML and javascript.

Facebook itself is built on React which contains 50,000+ components in its app. There are two types of components that you can use in your React application. They are:-

  1. Class Components

  2. Functional Components (Mostly used)

Reat Hooks are used in Functional Components to enhance their capabilities. Let me explain what are React Hooks.

What are React Hooks?

In React, only class components had the feature to use states. These components were called stateful. They had a component Lifecycle like componentDidMount() ,componentWillUpdate() and so on.

But from React version 16.8, functional components got into the limelight as React Hooks were introduced. Functional components were lighter and easy to use and hooks made them powerful. Hooks gave functional components to perform tasks like class components. It allows functional components to access the state and other React features like side-effects etc.

Types of Hooks we are going to see:-

  1. useState Hook

  2. useEffect Hook

  3. useContext Hook

  4. useRef Hook

  5. useMemo Hook

  6. useCallback Hook

  7. useReducer Hook

useState() Hook

The useState hook allows us to use states in functional components. It allows us to update elements or values without manipulating the Browser's DOM. Let's see an example.

import React, {useState} from "react";

const ShowInputText = () => {
    const [name, setName] = useState("");

    return (
        <div>
            <input type="text" value={name} 
             onChange={(e) => setName(e.target.value)} 
             />
            <p>Hello {name}! Welcome to the Hooks tutorial.</p>
        </div>
    );
}
// This is how we import the useState from react library.
import React, {useState} from "react";
// This is how we initialize useState
const [name, setName] = useState("");

useState() provides us with two things.

  • name is the variable to which the desired value is to be assigned.

  • setName is the method/function by which we assign the desired value to name.

You can give the variable and method any name but set is a convention that is used before the method/function.

<input type="text" value={name} 
onChange={(e) => setName(e.target.value)} // Setting the value of name variable with setName method.
/>

As you can see in the above code, I have assigned the value of input to the variable name. When the user will type in the input, the value of name will change as onChange={(e) => setName(e.target.value)} is setting the value of input to name using setName method/function.

As, the user will type we will be seeing the output in <p>Hello {name}! Welcome to the Hooks tutorial.</p>.

useEffect() Hook

Before starting with useEffect let me tell give you a brief about side effects.

When we want to reach or talk to the outside world such as by fetching data or sending data through our React app, so the change of state that happens due to these processes is called Side Effects.

The useEffect() hook is used to handle side effects in a functional component. Let's see an example of fetching from an API.

import React, {useState, useEffect} from "react";

const RandomQuote = () => {
    const [quote, setQuote] = useState("");
    const [author, setAuthor] = useState("");

    async function fetchQuote() {
        let response = await fetch("https://api.quotable.io/random");
        response = await response.json();
        setQuote(response.content);
        setAuthor(response.author);
    }

    useEffect(() => {
        fetchQuote();
    }, []);

    return (
        <div>
            <p>{quote}</p>
            <small>By:- {author}</small>
        </div>
    )
}
// This is how you import useEffect
import React, {useState, useEffect} from "react";
const [quote, setQuote] = useState("");
const [author, setAuthor] = useState("");

Initializing two variables using useState() quote and author to store the incoming data in these.

The below function is to fetch the data.

    async function fetchQuote() {
        let response = await fetch("https://api.quotable.io/random");
        response = await response.json();
        setQuote(response.content); // setting quote variable using setQuote method.
        setAuthor(response.author); // setting author variable using setAuthor method.
    }
    useEffect(() => {
        fetchQuote(); // fetching quotes
    }, []);

In the above code, the syntax of useEffect() can be seen. We are giving it two arguments a callback function and an empty array.

In the callback function as you can see we are calling the fetchQuote function. This function will be invoked after the component has been loaded.

The second argument, an array, makes sure that useEffect() hook runs only one time. Here, you can pass the states that when these states change the useEffect() hook will run. Let me modify the above example to explain the array.

import React, {useState, useEffect} from "react";

const RandomQuote = () => {
    const [quote, setQuote] = useState("");
    const [author, setAuthor] = useState("");
    const [get, setGet] = useState(false);

    async function fetchQuote() {
        let response = await fetch("https://api.quotable.io/random");
        response = await response.json();
        setQuote(response.content);
        setAuthor(response.author);
        setGet(false);    
    }

    useEffect(() => {
        if (get) {
          fetchQuote();
        }
    }, [get]);

    return (
        <div>
            <p>{quote}</p>
            <small>By:- {author}</small>
            <button onClick={() => setGet(true)}>Get Quote</button>
        </div>
    )
}

Here, in the above example as you can see, when the get state will change and is set to true on the button click only then the data will be fetched from the API.

You can also unsubscribe to the events that we have triggered after the component render using useEffect(). Let me give you an example.

import React, {useState, useEffect} from "react";

const RandomQuote = () => {
    const [quote, setQuote] = useState("");
    const [author, setAuthor] = useState("");
    const [get, setGet] = useState(false);

    async function fetchQuote() {
        let response = await fetch("https://api.quotable.io/random");
        response = await response.json();
        setQuote(response.quote);
        setAuthor(response.author);
        setGet(false);    
    }

    useEffect(() => {
        if (get) {
          fetchQuote();
        }
    }, [get]);

    useEffect(() => {
        let timer = setInterval(() => {
            setGet(true);
        }, 6000);
        return () => clearInterval(timer);
    })

    return (
        <div>
            <p>{quote}</p>
            <small>By:- {author}</small>
        </div>
    )
}

As, you can see instead of clicking on a button we are fetching quotes based on a timed interval. So, after every 6 sec a new quote will be fecthed. If we will not call off the setInterval, it will run in the background and will cause the app to slow down. To tackle this problem we return an anonymous function to clear the setInterval event.

    useEffect(() => {
        let timer = setInterval(() => {
            setGet(true);
        }, 6000);
        return () => clearInterval(timer); // the return statement that unsubscribes the timer event.
    })

useContext() Hook

The useContext hook helps the components to access a state variable from anywhere, without initializing it in a lexical environment. It majorly contributes in solving the problem called prop drilling.

Prop Drilling is the passing of props from parent to the very last children.

Let's say you have 10 components and 1 is the parent component of 2, 2 is the parent component of 3 and so on. We are getting data at 1st component, and we need to use that data in the very last children, i.e., 10th component. So, to make this happen we will pass the data to all the children of 1st component in between to reach the last one. The maintainability of the code reduces to a great extent.

To solve this we use useContext() hook. Let's see an example.

The below code shows how to initialize the createContext for using it in all the components.

// src/context/userContext.js
import {createContext} from "react";

const userContext = createContext();
export default userContext;

Let's consider a situation in which you have 2 components one is called in App.js component Component1 and the other one Component2 is called in Component1. You are getting a user's name in App.js and you need it in Component2.

// App.js
import React, {useState} from "react";
import Component1 from "./src/Component1";
import userContext from "./context/userContext";

const App = () => {
    const user = "John Doe";
    return (
        <userContext.Provider value={{ user }}>
            <div>
                <Component1 />
            </div>
        </userContext.Provider>
    )
} 

export default App;

The context that we defined in userContext.js is to be made available for the other components to be used in the component tree. So, to provide the user's name to all the components we need to use a Provider provided by context API itself. The below code shows how we can make it available to all components.

        <userContext.Provider value={{ user }}>
            <div>
                <Component1 />
            </div>
        </userContext.Provider>

Create a tag <userContext.Provider> </userContext.Provider> and pass an attribute named the value which will contain all the data that you need to access in every component.

// src/Component1.js
import React from "react";
import Component2 from "./src/Component2";

const Component1 = () => {
    return (
        <div>
            <Component2 />
        </div>
    )
} 

export default Component1;
// src/Component2.js
import React, {useContext} from "react";
import userContext from "./context/userContext";

const Component2 = () => {
    const {user} = useContext(userContext);
    return (
        <div>
            {user}
        </div>
    )
} 

export default Component2;

The below code shows how we access the global values in the context we created at the beginning. As you can see I destructed the value that I received through the provider.

const {user} = useContext(userContext);

And you can use the user anywhere in the component.

useRef() Hook

Refs are special attributes that are available on every element tag in JSX template. They allow us to reference any element tag when the component renders.

useRef() allows us to use the Refs easily and helps us to access/interact directly with the values or any other javascript attributes of that particular element.

Let's see an example.

import React, {useRef} from "react";

const App = () => {
    const inputRef = useRef();
    function handleClick() {
        console.log(inputRef.current.value);
        inputRef.current.value = "Hello World";
        inputRef.current.focus();
    }
    return (
        <div>
            <input 
            ref={inputRef}
            type="text"
            />
            <button onClick={handleClick}>Click Me</button>
        </div>
    )
} 

export default App;

In the above code, I have referenced an input using the useRef() hook which in this case is assigned as inputRef. Using inputRef, you can access the other features/javascript attributes.

useRef() provides a current object which has the access to all the required attributes.

inputRef.current.value = "Hello World";

As you can see you can change the value in the input by accessing the current object.

useMemo() Hook

useMemo() is a hook to handle the expensive processes/operations that can affect the performance of the Component to render. It caches the returned value of an expensive function.

Let's see an example. In this example, I have performed two things, one is the increment and decrement counter and the other one is changing the theme color of a paragraph.

import React, {useState, useMemo} from "react";

const Component = () => {

    const [count, setCount] = useState(0);
    const [theme, setTheme] = useState("light");

  let calculateNum = () => {
    let num = 0;
    for (let i = 0; i < 100000000; i++) {
      num += 1;
    }
    return num;
  }

    let num = useMemo(calculateNum(), []);

    const handleInc = () => {
        setCount(count+1);
    }

    const handleDec = () => {
        setCount(count-1);
    }

    let style = theme === "light" ? {
        backgroundColor: "white",
        color: "black"
    } : {
        backgroundColor: "black",
        color: "white"
    }

    return (
        <div>
            <p>{count}</p>
            <button onClick={handleInc}>Increment</button>
            <button onClick={handleDec}>Decrement</button>

            <p>Expensive Function: {num}</p>

            <p style={style}>Hello World!</p>
            <button onClick={theme === "light" ? setTheme("dark") :                 setTheme("light")}>Change Theme</button>
        </div>
    );

}
    const [count, setCount] = useState(0);
    const [theme, setTheme] = useState("light");

There are two state values used in this component, one is to set the counter integer and the other is to change the theme.

    const handleInc = () => {
        setCount(count+1);
    }

    const handleDec = () => {
        setCount(count-1);
    }

As you can see we are handling increments with handleInc and the decrements with handleDec. As you have noticed calculateNum function called in both the function handleInc and handleDec, it is used to mimic a heavy/expensive function that can make a component's performance slow to re-render.

    let style = theme === "light" ? {
        backgroundColor: "white",
        color: "black"
    } : {
        backgroundColor: "black",
        color: "white"
    }

I am also performing a theme change where on a click the theme will change to dark and light.

  let calculateNum = () => {
    let num = 0;
    for (let i = 0; i < 100000000; i++) {
      num += 1;
    }
    return num;
  }
  let num = useMemo(calculateNum(), []);

The problem is if I will not use useMemo() hook the value will keep returning itself on every re-render which inturn leads to invoking the expensive function. As you can see I have wrapped the calculateNum in the useMemo() as the function is very expensive to re-render every time. Here, useMemo() hook will keep the value in cache and will run only when some value or return value of the function will change.

useCallback() Hook

useCallback() hook is very similar to the useMemo(), instead of caching the returned value, useCallback() caches the whole function. It uses a technique called memoization.

Memoization is a technique in which partial results are recorded/stored for later use.

useReducer() Hook

useReducer() is a powerful state management hook just like useState() but with external functionalities. It relies on a function called reducer.

useReducer() is very similar to useState() in many ways so why use useReducer(), because it helps you to manage the state across multiple components.

Yes, you can also do this with useContext() hook but if you changed the value of the state it will re-render all the components that are below the component that contains this context.

Let's see an example of useReducer(). In this example, I have kind of made a TODO application with only two operations Add and Delete to keep it simple.

import React, {useState, useReducer} from "react";

let initialState = [];

function reducer(state, action) {
    switch action.type:
        case "ADD_TASK":
            return [
                {
                    id: state.length + 1,
                    task: action.task,
                    urgent: action.urgent
                }, ...state
            ];
        case "DELETE_TASK":
            let state = state.filter((el) => el.id !== action.id);
            return state;
        default:
            return;
}

const Component = () => {
    const [state, dispatch] = useReducer(reducer, initialState);
    const [task, setTask] = useState("");
    const [urgent, setUrgent] = useState(false);

    const deleteTask = (id) => {
        dispatch({
            type: "DELETE_TASK",
            id: id
        })
    }

    const handleSubmit = () => {
        dispatch({
            type: "ADD_TASK",
            task: task,
            urgent: urgent
        });
    }

    return (
        <div>
            <form onSubmit={handleSubmit}>
                <input type="text" onChange={(e) => setTask(e.target.value)} value={task} />
                <label>
                    <input type="checkbox" checked={urgent} onChange={setUrgent(!urgent)} /> {" "} <span>Urgent</span>
                </label>
                <button type="submit">Add</button>
            </form>
        </div>

        <div>
            {state.map((el) => {
                    return <div> 
                        <p style={{ color: urgent ? "red" : "black"; }}>{el.task} {"  "} <button onClick={() => deleteTask(el.id)}>Delete</button></p>
                    </div>
                }
            )}
        </div>
    );
}

As you can see in the below code a function called reducer is defined. It handles all the states that you provide to it.

Before starting you have to define an initialState of the state. The reducer contains an argument called action. action is used when you want to change/update the current state, this is very similar to when we use setState to set the value using useState().

let initialState = [];

function reducer(state, action) {
    switch action.type:
        case "ADD_TASK":
            return [
                {
                    id: state.length + 1,
                    task: action.task,
                    urgent: action.urgent
                }, ...state
            ];
        case "DELETE_TASK":
            let state = state.filter((el) => el.id !== action.id);
            return state;
        default:
            return;
}

The above code has a reducer function that contains a switch statement, in that we have defined action.type. So, whenever we will update the state it will be based on this type what processing that will be done to the current state.

// To Add the task to the state array       
  case "ADD_TASK":
            return [
                {
                    id: state.length + 1,
                    task: action.task,
                    urgent: action.urgent
                }, ...state
            ];
// To delete a task from the state array.        
case "DELETE_TASK":
            let state = state.filter((el) => el.id !== action.id);
            return state;

The below code shows how to initialize the useReducer() hook to perform certain actions on the state.

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

We passed two arguments to the hook, one is the reducer function that we saw before and another one is the initialState i.e. an array, in this case. We get two variables one is state and the other is dispatch. state is set to the initialState and dispatch is a function that handles the actions which will change/update the current state.

const Component = () => {
    const [state, dispatch] = useReducer(reducer, initialState);
    const [task, setTask] = useState("");
    const [urgent, setUrgent] = useState(false);

    const deleteTask = (id) => {
        dispatch({
            type: "DELETE_TASK",
            id: id
        })
    }

    const handleSubmit = () => {
        dispatch({
            type: "ADD_TASK",
            task: task,
            urgent: urgent
        });
    }

    return (
        <div>
            <form onSubmit={handleSubmit}>
                <input type="text" onChange={(e) => setTask(e.target.value)} value={task} />
                <label>
                    <input type="checkbox" checked={urgent} onChange={setUrgent(!urgent)} /> {" "} <span>Urgent</span>
                </label>
                <button type="submit">Add</button>
            </form>
        </div>

        <div>
            {state.map((el) => {
                    return <div> 
                        <p style={{ color: urgent ? "red" : "black"; }}>{el.task} {"  "} <button onClick={() => deleteTask(el.id)}>Delete</button></p>
                    </div>
                }
            )}
        </div>
    );
}

So, I have created a component that returns a form that has one input to enter the task, a checkbox for how urgent the task and a button to submit. I am also mapping the state as it is an array to display all the tasks that have been added. It displays all the tasks and contains a delete button if the task is completed.

    const [task, setTask] = useState("");
    const [urgent, setUrgent] = useState(false);

These states are used to handle the inputs in the form.

We have two functions to perform some processes.

    const deleteTask = (id) => {
        dispatch({
            type: "DELETE_TASK",
            id: id
        })
    }

    const handleSubmit = () => {
        dispatch({
            type: "ADD_TASK",
            task: task,
            urgent: urgent
        });
    }

As you can see, the form submitted is handled by handleSubmit. So, when the form is submitted with some input there is a dispatch function called with an argument. dispatch expects an action as an argument, so we are passing an object of the type of process to perform, the task itself and the urgent to indicate its urgency.

There is another function deleteTask, which is similar to the handleSubmit that we are dispatching an action. But, in this case, we are passing different keys to perform the removal of the task by id from the current state.