How to Use React Hooks – A Step-by-Step Guide for Beginners

React Hooks opens a new world in the React ecosystem. However, it can be quite confusing for beginners working with React for the first time. 

Therefore, in this tutorial, I’ll help you understand what React Hooks are, the various kinds of hooks available in React, and when and how to use them. Additionally, you will learn how to create custom hooks in React.

What are React Hooks?

React Hooks, put simply, are basic JavaScript functions that manage various tasks in React. That’s right, they’re not as complex as you might think. 

So, why are people so excited about them? Let’s find out.

In traditional React development, managing component state and handling side effects was primarily done within class components. (Don’t worry about the terms “state” and “side-effect” ; we’ll learn about them later in this post). However, this approach had some limitations and made code harder to read and maintain.

For example, just look at the code below and see how overwhelming it was to manage the state before the introduction of hooks. We had to manage states by binding them to the component using the JavaScript this keyword. 

React-Hooks

So finally, in the React v16.8 release, hooks were introduced to address these issues and provided a more elegant solution for functional components.

Introducing useState and useEffect Hook

In ReactJS, there are hooks such as useState, useEffect, useReducer, useMemo, useRef, and many others. In this post, we will try to understand the two most commonly used hooks: useState and useEffect.

Fun Fact: Do you know you can create your own React hooks? Well, I also guide you through creating your custom hooks shortly.

Understanding useState Hook

The useState hook in React allows the functional component to manage and manipulate state data. So, you might be wondering what the “state data” here is.

Okay, let me tell you, the state data here refers to the data within a component that changes over time through events or external effects, like HTTP calls.

For example, clicking on pause in a video player should pause the current playing video, or selecting a colour from the colour picker should select the current colour.

React-Hooks-for-Beginners

To do this, the components need to remember things, here: the current state of the video, and the current colour value.

Now, let’s use the useState hook using the example of the colour picker above and build a short application step by step.

  • To start, create a ColorPicker component in your React app. For now, let’s keep it simple – add a <h1> with the text “React Color Picker” (feel free to use any text you prefer).
export default function ColorPicker() {
  return (
    <main>
      <h1>React Color Picker</h1>
    </main>
  )
}
  • Now, import the useState hook from React. Remember, to call the useState hook inside the ColorPicker component, it cannot be called outside the ColorPicker component. Here, we have assigned the useState hook to the colorState variable and console log the colorState variable.
import { useState } from "react";

// const state = useState();  ❎ Cannot be use outside ColorPicker
export default function ColorPicker() {
  const colorState = useState() //✅ Correct way of calling useState hook
  console.log('Color State : ', colorState)     
  return (
    <main>
      <h1>React Color Picker</h1>
    </main>
  )
}
  • Next, open your browser’s console, and you’ll notice something like “Color State: ▶ Array(2).” If you expand this array, you’ll see that at index 0, there’s “undefined,” and at index 1, there’s a function.
Color State :Array(2)
                0 : undefined
                1 : ƒ ()
  • Now, let’s destructure the array that we get from the useState hook. We’ll assign the variables color and setColor to the elements at index 0 and 1, respectively, as returned by the useState hook.
import { useState } from "react";

export default function ColorPicker() {
  // const colorState = useState()
  const [color, setColor] = useState()
  return (
    <main>
      <h1>React Color Picker</h1>
    </main>
  )
}
  • When we call the useState hook, it takes a single argument known as initialState. This initial state can be of various types: an array, an object, a string, a boolean, a number, or pretty much anything. In our example, we set the initialState to ‘#8C00FF‘ and assign it to the color variable. Additionally, setColor is the function we use to modify the initialState value whenever we interact with it.
import { useState } from "react";

export default function ColorPicker() {
  // const colorState = useState()
  const [color, setColor] = useState('#8C00FF')
  return (
    <main>
      <h1>React Color Picker</h1>
    </main>
  )
}
  • Let’s take the next step. Add a <div> and style it to display the selected color, and include an <input> tag of type “color” for selecting the colour from the input. Set the value property of the input to the color (initialState), and attach an onChange function called onColorChange() to update the initial state.

When the input value changes, the onColorChange() function is triggered. Inside this function, we call the setColor function and pass e.target.value as an argument to it.

This will cause the setColor function to update the color value, which in turn updates and reflects the color change wherever it’s used.

import { useState } from "react";

export default function ColorPicker() {
  const [color, setColor] = useState('#8C00FF')
 
  function onColorChange(e){
    setColor(e.target.value)
  }

  const divStyle = {
    width : '200px',
    height : '200px',
    backgroundColor : color // color variable (#8c00ff)
  }

  return (
    <main>
      <h1>React Color Picker</h1>
        <div style={divStyle}>
          <h2>Color : {color}</h2>
        </div>
        <input type='color' value={color} onChange={onColorChange} />
    </main>
  )
}
  • Here’s an important point to remember: when the state changes, in this case, the color state, the component re-renders, and as a result, the user interface (UI) is updated to reflect the new state. This dynamic behaviour is one of the key strengths of React and its useState hook, allowing for responsive and real-time updates in our web applications. 
React-Hooks-for-Beginners

Understanding useEffect Hook

useEffect Hook allows us to execute code after the component has been rendered. It is useful for tasks like data fetching in case you are not using any server data management library like React Query,  etc. You can also use it for setting up subscriptions or handling side effects like using any external libraries, e.g a video plugin.

The syntax for useEffect hook is very simple, it takes a callback function and a dependency array, and the dependency array controls when the effect should run. 

useEffect(callbackFunction, dependencyArray);

Now, the dependency array can either be empty or we can pass values such as props, state, and contexts from the component scope. A useEffect hook with an empty dependency array will run only for the first time when the component renders and if the useEffect hook has a dependency array, it will run every time the value in the dependency array changes.

Hooks-in-React

Note: We can also neglect the dependency array, however it can cause major performance issues as the effect will run after every render of the component, including the initial one, without relying on any specific values.

Now, let’s see how we can use this hook in our application.

  • Create a component let’s say Todo and import useEffect from React. To use the useEffect, call it inside the Todo component.
import { useEffect } from "react";

export default function Todo() {
  useEffect(() => {}, []);
//useEffect(callBackFunction, [dependenciesArray]);

  return (
    <div>
    <h1>Todo App</h1>
    </div>
  )
}
  • Let’s add a callback function that fetches some data from an external API.
import { useEffect } from "react";

export default function Todo() {
 
  //callBackFunction to fetch todods
  function fetchTodos(){
    fetch('https://jsonplaceholder.typicode.com/todos')
    .then(response => response.json())
    .then(json => console.log(json))
  }

  //useEffect(callBackFunction, [dependenciesArray]);
  useEffect(fetchTodos, []); 

  return (
    <div>
    <h1>Todo App</h1>
    </div>
  )
}
  • Let’s show this data to the screen. To do that we need a useState hook to store the data, initially, it is an empty array and it gets updated after the effect runs successfully. 
import { useEffect } from "react";

export default function Todo() {
  const [todos, setTodos] = useState([])

  //callBackFunction to fetch todods
  function fetchTodos(){
    fetch('https://jsonplaceholder.typicode.com/todos')
    .then(response => response.json())
    .then(json => setTodos(json))
  }

  //useEffect(callBackFunction, [dependenciesArray]);
  useEffect(fetchTodos, []); 

  return (
    <div>
    <h1>Todo App</h1>
    </div>
  )
}
  • Now, map over the todos and show the data to the user. It will show ‘loading…’ until the components fetch the data from the API.
import { useEffect } from "react";

export default function Todo() {
  const [todos, setTodos] = useState([])

  //callBackFunction to fetch todods
  function fetchTodos(){
    fetch('https://jsonplaceholder.typicode.com/todos')
    .then(response => response.json())
    .then(json => setTodos(json))
  }

  //useEffect(callBackFunction, [dependenciesArray]);
  useEffect(fetchTodos, []); 

  return (
    <div>
    <h1>Todo App</h1>
    {!todos.length && <h1>loading...</h1>}
    <ul>
      {todos?.map(({id, title}) => (
        <li key={id}>{title}</li>
      ))}
    </ul>
    </div>
  )
}
What are hooks in React

Cache Your Data using useMemo and useCallback Hooks


By now, you might be familiar that whenever the state changes the component re-renders which ensures that the User Interface(UI) stays up to date. However, re-rendering components can be resource-intensive and it might affect the overall performance of your application. 

For example, suppose in a component of your app there is a button that opens a modal and also there is a heavy calculation going on, when you open or close the modal you may not want to run the heavy calculation.

So, to solve this problem you can cache or memoize the data that requires resource-intensive operations and in React you can do that using useMemo and useCallback hooks. Let’s understand both of them one by one.

useMemo – memoization for value caching

useMemo Hook caches the calculated value of an operation whenever the state changes or re-render occurs. 

The syntax to use useMemo hook is: 

const cachedValue = useMemo(calculateValue, dependencies)
  • calculateValue : the function that will calculate the value that you want to cache, it should be a pure function and should not take any argument.
  • dependencies : on which the calculateValue is dependent on and will be called whenever these dependencies change, it can be props, states or the variables or functions declared inside that component.
Hooks

useCallback- memoization for function caching

The useCallback hook is another function that enables us to cache data within React applications. It accepts a function and its array of dependencies. Like the useMemo hook, the function runs every time the dependencies change.

const cachedFunction = useCallback(function, [dependencies])

Let’s look at the example below:

Within the SearchProduct component, the useCallback hook keeps track of the searched item and ensures that the right item is retrieved from the array.

Managing states with the useReducer Hook

The useReducer hook is another great hook for managing states within React applications. However, unlike the useState hook, it is commonly used in components with numerous states across multiple event handlers.

The useReducer hook has four main components: the state, the reducer function, the action, and the dispatch function. 

The reducer function manipulates the state directly and returns a copy of the result, and the dispatch function triggers the reducer function when various events occur.

The action is an object containing a type and a payload property. The type property specifies the exact action to be executed by the reducer function, and the payload can accept data from the user or other parts of the application.

Let’s consider an example using a contact form that accepts a name, email address, and message from the users.

import { useReducer } from "react";

const ACTIONS = {
    UPDATE_NAME: "updateName",
    UPDATE_EMAIL: "updateEmail",
    UPDATE_MESSAGE: "updateMessage",
};

const reducer = (state, action) => {
    switch (action.type) {
        case ACTIONS.UPDATE_NAME:
            return { ...state, name: action.payload.value };
        case ACTIONS.UPDATE_EMAIL:
            return { ...state, email: action.payload.value };
        case ACTIONS.UPDATE_MESSAGE:
            return { ...state, message: action.payload.value };
        default:
            return state;
    }
};

export default function App() {
    const [state, dispatch] = useReducer(reducer, {
        name: "",
        email: "",
        message: "",
    });

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log({ state });
    };

    return (
        <div>
            <h2>Contact Us</h2>
            <form onSubmit={handleSubmit}>
                <label>Full Name</label>
                <input
                    type='text'
                    value={state.name}
                    onChange={(e) => {
                        dispatch({
                            type: ACTIONS.UPDATE_NAME,
                            payload: {
                                value: e.target.value,
                            },
                        });
                    }}
                />
                <label>Email Address</label>
                <input
                    type='email'
                    value={state.email}
                    onChange={(e) => {
                        dispatch({
                            type: ACTIONS.UPDATE_EMAIL,
                            payload: {
                                value: e.target.value,
                            },
                        });
                    }}
                />
                <label>Message</label>
                <textarea
                    rows={6}
                    value={state.message}
                    onChange={(e) => {
                        dispatch({
                            type: ACTIONS.UPDATE_MESSAGE,
                            payload: {
                                value: e.target.value,
                            },
                        });
                    }}
                />
                <button type='submit'>SEND</button>
            </form>
        </div>
    );
}

The code snippet above accepts the email, username, and message from the user and stores them in a state using the useReducer hook.

Let me explain better:

const [state, dispatch] = useReducer(reducer, {
        name: "",
        email: "",
        message: "",
});

When declaring the useReducer hook, it accepts two parameters – the reducer function and the state object. 

const ACTIONS = {
    UPDATE_NAME: "updateName",
    UPDATE_EMAIL: "updateEmail",
    UPDATE_MESSAGE: "updateMessage",
}

const reducer = (state, action) => {
    switch (action.type) {
        case ACTIONS.UPDATE_NAME:
            return { ...state, name: action.payload.value };
        case ACTIONS.UPDATE_EMAIL:
            return { ...state, email: action.payload.value };
        case ACTIONS.UPDATE_MESSAGE:
            return { ...state, message: action.payload.value };
        default:
            return state;
    }
};

The reducer function is in charge of updating the state values within a useReducer hook. It accepts the state and action as parameters.

The action parameter contains a payload object holding the current value of the input field. It also has a type property that enables us to update the right state when there are numerous states to update.

Finally, each input field triggers the reducer function via the dispatch function. The dispatch function accepts an object containing the action type and payload required by the reducer function.

              <input                                 type='text'
                    value={state.name}
                    onChange={(e) => {
                        dispatch({
                            type: ACTIONS.UPDATE_NAME,
                            payload: {
                                value: e.target.value,
                            },
                        });
                    }}
                />

Understanding Reusable Components by Creating Your Own Hook

Introduction to Custom Hooks

So far, we’ve been using the built-in hooks provided by React. However, there are scenarios where you might require custom hooks tailored for specific purposes.

For example, you might need a hook to determine if a user is online, to check if your app is running in the background, or for other specialised tasks.

But why and when should you create a custom hook? Imagine you have an application where one component is performing a CPU-intensive task, and you want to reduce its impact when the user switches to a different application tab. Meanwhile, another component is handling animations, and you want to stop the animation when the tab is behind some other application. 

Custom hooks in React

One option is to write code in each of these components to monitor their visibility, but a more organised and efficient approach is to create a custom hook to handle this common task. In this way we can abstract common functionalities, making our code cleaner and easier to manage and that’s the main purpose of creating a custom hook.

Building the Custom Hook

Let’s build the custom hook usePageVisibility to check if the app is visible or not.

Remember to use use before the name of any custom hook you build.

For simplicity, we have written the usePageVisibility in the same component, you can write it in a different folder and use it wherever you need.

Let’s understand the usePageVisibility hook :

import './App.css'
import { useState, useEffect } from "react";

// Custom hook to check if the tab is in the background
function usePageVisibility() {
  const [isVisible, setIsVisible] = useState(true);

  function handleVisibilityChange() {
    setIsVisible(document.visibilityState === 'visible');
  };

  useEffect(() => {
    document.addEventListener('visibilitychange', handleVisibilityChange);

    return function(){
      document.removeEventListener('visibilitychange', handleVisibilityChange);
    };
  }, []);

  return isVisible;
}

export default function App() {
  const isTabVisible = usePageVisibility();

  return (
    <div>
      <h1>Check Page Visibility</h1>
      <p>Is the tab visible? { isTabVisible ? 'Yes' : 'No' }</p>
    </div>
  );
}

  • It uses the useState hook to create a state variable isVisible which represents whether the page is currently visible (it is true by default).
  • The handleVisibilityChange function is responsible for updating the isVisible state based on changes in the page’s visibility state (visible or hidden).
  • The useEffect hook is used to:
    • Add an event listener for the ‘visibilitychange’ event on the document. This event is triggered when the page’s visibility state changes.
    • Inside the useEffect, a cleanup function is returned. This function removes the event listener from the component using the hook when the dependencies change.
  • The hook returns the isVisible state, providing a boolean value to indicate whether the web page is currently visible to the user or not.

We are calling the usePageVisibility hook in the `App` and it is assigned to the isTabVisible, the value of isTabVisible will be either true or false. Based on this variable we are printing “Yes” or “ No” on the UI. 

React-Custom-Hooks

Common Mistakes to Avoid

Using React hooks effectively can greatly improve the development of React applications. However, there are common mistakes and pitfalls that you make and can avoid to prevent unexpected issues. Here are some common mistakes to watch out for when using React hooks:

  • Always call hooks at the top level of a function component or in other custom hooks. Avoid calling hooks inside loops, conditions, or nested functions.
export default function App(){
  // Call your hooks here
}

  • When using useEffect, make sure to specify the dependencies as the second argument. Omitting dependencies can lead to unexpected behaviour.
useEffect(() => {
  // Missing dependencies can lead to bugs
}, []);

  • Be cautious when using useState inside useEffect, as it can lead to infinite render loops if not managed properly.
useEffect(() => {
  // Causes an infinite render loop without dependency management
  setState(state + 1);
}, []);

  • Do not modify state variables directly. Always use the state updater function returned by useState to update the state.
state.property = 'new value'; // Incorrect, don't modify state directly

  • Refactor common logic into custom hooks when appropriate. Not doing so can lead to code duplication and reduced maintainability.
const MyComponent = () => {
  // Common logic duplicated in multiple components
  useEffect(() => {
    // Common logic
  });
};

Conclusion

React hooks have changed the way we manage state and side effects in functional components. These are just plain JavaScript functions that simplify and enhance the development of React applications.

Here’s a quick recap of the key points we’ve covered in this post:

  • React hooks are one of the essential concepts in React, and understanding it will help you to build applications in an efficient way..
  • In this post, we explored two of the most commonly used hooks, useState and useEffect.
    • useState allows us to manage and manipulate component state, which represents the specific memory of the component that changes over time as users interact with our application.
    • useEffect hook is used for managing and keeping track of various actions within your components, such as making API calls, and monitoring when a component mounts and unmounts within the application.
  • The useReducer hook is used for managing states within applications with complex state management and transitions.
  • You’ve also learned how to create custom hooks for specific tasks, such as checking if the web app tab is in the background. Custom hooks help us to reuse logic across different components.
  • We have also learned how to avoid some common mistakes that many beginners might face, like : not following the rules of hooks, neglecting dependencies in the dependency array, causing infinite render loops, incorrect usage of state updater functions, and more.

I hope this post is helpful and you have learned about how to use hooks in your React app.

Do share on your socials if you find it helpful.

If you want to read more about ReactJS and JavaScript then start reading some of recent articles.

Leave a Comment

Your email address will not be published. Required fields are marked *