DEV Community

Kingsley Amankwah
Kingsley Amankwah

Posted on

A Comprehensive Guide to React Hooks: Simplifying State and Side Effects

Introduction to React Hooks:

React Hooks have revolutionized the way we write React components by providing a simpler and more elegant approach to managing state and handling side effects. In this article, we will explore the core hooks in React and dive into custom hooks, advanced hook patterns, and best practices. Let's get started!

Benefits of using Hooks over class components

Before the introduction of React Hooks, managing state and side effects in functional components was challenging. Class components required complex lifecycle methods and often led to verbose and convoluted code. React Hooks solve these problems by offering a more intuitive and functional approach. With Hooks, we can write cleaner, more readable code and enjoy benefits such as improved reusability, code organization, and performance optimizations.

Understanding the Core Hooks:

1. useState Hook:

The useState hook allows us to introduce stateful logic into functional components. It takes an initial state value as an argument and returns an array with the current state value and a function to update the state. Let's see an example of creating a counter component using useState:

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

In the above code, we initialize the count state to 0 using useState. The count value is displayed, and the setCount function is used to update the count when the button is clicked.

2. useEffect Hook

The useEffect hook enables us to perform side effects in functional components, such as fetching data from an API, subscribing to events, or manipulating the DOM. It accepts a callback function and an optional dependency array to control when the effect runs. Let's see an example of fetching data from an API using useEffect:

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

function DataFetcher() {
  const [data, setData] = useState([]);

  useEffect(() => {
    fetch('https://api.example.com/data')
      .then(response => response.json())
      .then(data => setData(data));
  }, []);

  return (
    <div>
      {data.map(item => (
        <p key={item.id}>{item.name}</p>
      ))}
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

In this code, we use useEffect to fetch data from an API when the component mounts. The fetched data is stored in the state using the setData function, and it is rendered in the component.

3. useContext Hook

The useContext hook provides a way to access shared state or data without prop drilling . It allows us to create a context and consume it in any component within the context provider. Let's see an example of creating a theme-switching component using useContext:

import React, { useContext } from 'react';

const ThemeContext = React.createContext('light');

function ThemeSwitcher() {
  const theme = useContext(ThemeContext);

  return (
    <div>
      <p>Current theme: {theme}</p>
      <button onClick={() => console.log('Switch theme')}>Switch Theme</button>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

In the code above, we create a ThemeContext using React.createContext and provide a default value of 'light'. The useContext hook is used in the ThemeSwitcher component to access the current theme value. When the button is clicked, it triggers a theme switch action.

Custom Hooks: Reusability and Abstraction:

Custom hooks are a powerful concept in React that allows us to extract and share stateful and reusable logic across multiple components. They provide a way to abstract complex logic into reusable functions. By creating custom hooks, we can enhance code reusability and make our components more modular and maintainable.

Building a form validation hook:

Let's build a custom hook for a form validation. The hook will handle the validation logic and return the validation state and functions to update the form values and trigger validation.

useFormValidation Custom Hook

import React, { useState } from 'react';

function useFormValidation(initialState, validate) {
  const [values, setValues] = useState(initialState);
  const [errors, setErrors] = useState({});
  const [isSubmitting, setSubmitting] = useState(false);

  const handleChange = event => {
    setValues({
      ...values,
      [event.target.name]: event.target.value
    });
  };

  const handleSubmit = event => {
    event.preventDefault();
    setErrors(validate(values));
    setSubmitting(true);
  };

  return { values, errors, isSubmitting, handleChange, handleSubmit };
}

Enter fullscreen mode Exit fullscreen mode

Explanation:
The useFormValidation custom hook handles the state and logic for form validation. It takes in initialState, which is an object representing the initial form values, and validate, which is a function for validating the form values. It returns an object with values, errors, isSubmitting, handleChange, and handleSubmit.

  1. values: represents the current form values.
  2. errors: holds any validation errors for the form.
  3. isSubmitting: indicates whether the form is being submitted.
  4. handleChange: is a function that updates the form values as the user types.
  5. handleSubmit: is a function that handles the form submission, triggers validation, and sets the errors and submitting state.

LoginForm Component


function validateLoginForm(values) {
  let errors = {};

    if (!values.email) {
    errors.email = 'Email is required';
  }

    if (!values.password) {
    errors.password = 'Password is required';
  } else if (values.password.length < 6) {
    errors.password = 'Password must be at least 6 characters long';
  }

  return errors;
}

function LoginForm() {
  const { values, errors, isSubmitting, handleChange, handleSubmit } = useFormValidation(
    { email: '', password: '' },
    validateLoginForm
  );

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="email">Email</label>
        <input
          type="email"
          id="email"
          name="email"
          value={values.email}
          onChange={handleChange}
        />
        {errors.email && <span>{errors.email}</span>}
      </div>
      <div>
        <label htmlFor="password">Password</label>
        <input
          type="password"
          id="password"
          name="password"
          value={values.password}
          onChange={handleChange}
        />
        {errors.password && <span>{errors.password}</span>}
      </div>
      <button type="submit" disabled={isSubmitting}>
        Submit
      </button>
    </form>
  );
}

export default LoginForm;

Enter fullscreen mode Exit fullscreen mode

Explanation:
The LoginForm component utilizes the useFormValidation custom hook to handle the form's state and validation. It also includes an example validation function, validateLoginForm, which validates the email and password fields.

Inside the component, we destructure the values, errors, isSubmitting, handleChange, and handleSubmit from the useFormValidation hook. These values and functions are then used within the JSX to render the form.

Advanced Hook Patterns:

1. useReducer Hook

The useReducer hook is an alternative to useState when managing more complex state that involves multiple actions. It follows the Redux pattern of managing state with reducers and actions. Let's see an example of implementing a todo list using useReducer:

import React, { useReducer } from 'react';

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { id: Date.now(), text: action.payload, completed: false }];
    case 'TOGGLE_TODO':
      return state.map(todo => (todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo));
    case 'REMOVE_TODO':
      return state.filter(todo => todo.id !== action.payload);
    default:
      return state;
  }
}

function TodoList() {
  const [todos, dispatch] = useReducer(todoReducer, []);

  const handleAddTodo = () => {
    const todoText = // Get todo text from input field
    dispatch({ type: 'ADD_TODO', payload: todoText });
  };

  return (
    <div>
      <input type="text" placeholder="Enter todo" />
      <button onClick={handleAddTodo}>Add Todo</button>

      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <span>{todo.text}</span>
            <button onClick={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}>
              {todo.completed ? 'Mark Incomplete' : 'Mark Complete'}
            </button>
            <button onClick={() => dispatch({ type: 'REMOVE_TODO', payload: todo.id })}>Remove</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

In this example, we have a todoReducer function that takes in the current state and an action, and based on the action type, it performs the necessary updates to the state. The reducer function handles three types of actions: adding a new todo, toggling the completion status of a todo, and removing a todo.

The TodoList component utilizes the useReducer hook by calling it with the todoReducer function and an initial state of an empty array []. The hook returns the current state (todos) and a dispatch function that we use to send actions to the reducer.

The handleAddTodo function is called when the "Add Todo" button is clicked. It retrieves the todo text from an input field (which is not included in this code snippet) and dispatches an action of type 'ADD_TODO' with the payload as the todo text. The reducer function then adds a new todo object to the state array, including an auto-generated ID, the todo text, and a default completion status of false.

The TodoList component renders an input field and a button for adding new todos. Below that, it renders a list of todos using the todos.map function. Each todo is rendered as an <li> element with its text displayed. Additionally, there are two buttons for each todo: one for toggling the completion status and another for removing the todo. When these buttons are clicked, corresponding actions are dispatched to the reducer to update the state accordingly.

2. useRef Hook

Accessing and persisting values across renders:
The useRef hook provides a way to persist values across renders without triggering a re-render. It can also be used to access DOM elements directly. Let's see an example of implementing an input field with useRef:

import React, { useRef } from 'react';

function InputWithFocus() {
  const inputRef = useRef(null);

  const handleClick = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>Focus Input</button>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

In this code, the inputRef is created using useRef and assigned to the input element's ref attribute. When the button is clicked, the handleClick function is triggered, which focuses the input element using the ref's current property.

Conclusion

React Hooks have transformed the way we write React components, offering a more streamlined and functional approach to managing state and handling side effects. By leveraging core hooks like useState, useEffect, and useContext, we can simplify our code and enhance reusability. Additionally, custom hooks and advanced hook patterns like useReducer and useRef provide powerful tools for building complex and optimized components.

As you embark on your React journey, I encourage you to explore and experiment with React Hooks in your projects. Their flexibility, simplicity, and performance benefits will undoubtedly elevate your development experience. Happy coding!

Top comments (2)

Collapse
 
akuoko_konadu profile image
Konadu Akwasi Akuoko

Thanks King 🙌
This was awesome, I'll try and use the useRwducer hooks in the coming weeks. One question though, can I use the useReducer hook in place of redux?

Collapse
 
kingsley profile image
Kingsley Amankwah

Sure