DEV Community

Cover image for Mastering React Hooks: A New Era of Elegance in Building React Components
Fred Zugs
Fred Zugs

Posted on • Updated on

Mastering React Hooks: A New Era of Elegance in Building React Components

Table of Contents

Introduction:

React Hooks have been an exceptional incorporation to the framework since their introduction. They offer a fresh and more efficient way to build React components without experiencing the problems associated with class-based components. In this article, we will delve into the realm of React Hooks, explaining their functionalities and the enhancements they bring to the development process.

Background/Context:

In the past, React development heavily depended on class components for handling state and effects.However, these class-based components had several limitations, including complex state management and limited form of reusability. React Hooks introduced in React 16.8 serves an improved alternative, providing a more powerful and efficient way to build robust and scalable applications.

Prerequisites:

Before learning React hooks, it's essential to have a solid grasp of these React fundamentals:

  • Components
  • Props
  • State
  • Lifecycle methods

In addition to these fundamentals, it is also helpful to be familiar with the following JavaScript concepts:

  • Functional programming
  • Higher-order functions

This article caters to those who have a good understanding of these concepts. If you wish to follow the examples as you read through, you should have Node installed and a React development environment set up in your preferred code editor.

Unraveling React Hooks:

Consider React Hooks as tools that help you build components quickly and easily. It has three major functions that make it uncomplicated to manage state and lifecycle in React. These functions are useState, useEffect, and useContext. They act as helpers that are easier to use compared with class components, so you can write cleaner and more straightforward code. Learn how to use React Hooks, as it's essential for building robust and easy-to-fix React apps. Here’s a breakdown of how React Hooks functions work compared with class components.

State Management with useState:

useState in React is a hook for adding state to functional components. It gives you an array with two things: the current state and a function to change it.

Before useState, establishing and managing state required explicit constructor methods and event handlers. Here's an example:

// Class Component
class Counter extends Component {
  constructor() {
    super();
    this.state = { count: 0 };
  }

  incrementCount = () => {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.incrementCount}>Increment</button>
      </div>
    );
  }
} 
Enter fullscreen mode Exit fullscreen mode

With the useState hook, state management becomes more straightforward:


import { useState } from 'react'


 // Functional Component with Hooks
 function App() {

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

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

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

}

Enter fullscreen mode Exit fullscreen mode

The useState Hook in React simplifies state management by eliminating the need for constructor methods and reducing boilerplate code. This means that you don't have to write a lot of code to do minimal jobs. It allows functional components to have state, making the code more concise, reusable, and flexible compared to class components.

Effect Management with useEffect:

useEffect in React is a hook for handling side effects in your components. Side effects are any operations that change the state of the outside world, such as fetching data, updating the DOM, or logging to the console.

Syntax for using useEffect::

useEffect accepts two arguments. These arguments include;

A function: This function is called after the component renders. You can use this function to perform your side effects.

An array of dependencies: This array specifies which values your side effects depend on. If any of the values in the array change, React will re-run your side effects.

In traditional class components, orchestrating effects, such as data fetching, often involved the use of lifecycle methods. For instance:

  // Class Component
class DataFetcher extends Component {
  constructor() {
    super();
    this.state = { data: null };
  }

  componentDidMount() {
    fetch('https://api.example.com/data')
      .then(response => response.json())
      .then(data => this.setState({ data }));
  }

  render() {
    return (
      <div>
        {this.state.data ? <p>Data: {this.state.data}</p> : <p>Loading...</p>}
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

With the useEffect hook, the arrangement of effects becomes streamlined within functional components:

// Functional Component with Hooks
function DataFetcher() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch('https://api.example.com/data')
      .then(response => response.json())
      .then(data => setData(data));
  }, []); // Empty dependency array means it runs only once (componentDidMount).

  return (
    <div>
      {data ? <p>Data: {data}</p> : <p>Loading...</p>}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Simplifying Data Sharing in React with useContext:

useContext is a React hook that provides a way to access context values without having to pass them down through props. This can be useful for sharing data between deeply nested components, or for making data available to all components in a component tree without having to explicitly pass it to each one.

To use useContext, you first need to create a context object using the createContext() function. This function takes two arguments: a default value for the context, and a callback function that returns the current context value.

Once you have created a context object, you can use it in your components by calling the useContext() hook. The useContext() hook in React takes a context object and gives you the current context value.

Prior to the use of useContext, class components required passing props down the hierarchy when your app gets larger and you need to pass data deep into your component.Here's an example:

  // ParentComponent.js
  import React, { Component } from 'react';
  import ChildComponent from './ChildComponent';

  class ParentComponent extends Component {
    constructor() {
      super();
      this.state = { value: 'Hello from Parent' };
    }

    render() {
      return <ChildComponent value={this.state.value} />;
    }
  }

  // ChildComponent.js
  import React, { Component } from 'react';

  class ChildComponent extends Component {
    render() {
      return <p>{this.props.value}</p>;
    }
  }

  export default ParentComponent;
Enter fullscreen mode Exit fullscreen mode

With useContext, you can create a shared context, making data accessible to deeply nested components without prop drilling:

  // MyContext.js
  import React from 'react';

  const MyContext = React.createContext();

  export default MyContext;
Enter fullscreen mode Exit fullscreen mode
  // ParentComponent.js
  import React, { useState } from 'react';
  import MyContext from './MyContext';
  import ChildComponent from './ChildComponent';

  function ParentComponent() {
    const [value, setValue] = useState('Hello from Parent');

    return (
      <MyContext.Provider value={value}>
        <ChildComponent />
      </MyContext.Provider>
    );
  }
Enter fullscreen mode Exit fullscreen mode
  // ChildComponent.js
  import React, { useContext } from 'react';
  import MyContext from './MyContext';

  function ChildComponent() {
    const value = useContext(MyContext);

    return <p>{value}</p>;
  }

  export default ChildComponent;
Enter fullscreen mode Exit fullscreen mode

The useContext hook simplifies the process of sharing data across components in a tree.

Creating Reusable Functionality with Custom Hooks:

Custom hooks are JavaScript functions that encapsulate reusable functionality and can be used throughout a React application. They are similar to React hooks, but they are not provided by React itself. Instead, they are developed by the community and can be shared and used by other developers.

Custom hooks can be used to implement a wide variety of functionality, such as:

  • Managing state
  • Handling side effects
  • Abstracting away complex logic
  • Providing reusable UI components

Custom hooks are created using the same syntax as React hooks, but they can also accept and return any type of data, including objects, arrays, and functions. This makes them very flexible and powerful.

Class components made it cumbersome to reuse stateful logic across multiple components. Custom hooks simplify this process. Here's an example of creating and using a custom hook for managing a "dark mode" state:

  // useDarkMode.js
  import { useState, useEffect } from 'react';

  function useDarkMode() {
    const [darkMode, setDarkMode] = useState(false);

    useEffect(() => {
      // Implement dark mode logic here...
    }, [darkMode]);

    return [darkMode, setDarkMode];
  }

  export default useDarkMode;
Enter fullscreen mode Exit fullscreen mode
  // DarkModeToggle.js
  import React from 'react';
  import useDarkMode from './useDarkMode';

  function DarkModeToggle() {
    const [darkMode, setDarkMode] = useDarkMode();

    return (
      <label>
        Dark Mode:
        <input type="checkbox" checked={darkMode} onChange={() => setDarkMode(!darkMode)} />
      </label>
    );
  }
Enter fullscreen mode Exit fullscreen mode

Custom hooks like useDarkMode make it easier to encapsulate and reuse complex stateful logic across components.

Optimizing State Management with useReducer

useReducer is a React hook that lets you manage your component's state in a predictable way. It takes a reducer function and an initial state value as arguments, and returns an array containing the current state and a dispatch function.
It's a good choice for managing state when you have a complex state that is difficult to manage with useState or need to share state between multiple components and even perform complex state updates, such as updating state variables at the same time.

How to use useReducer to improve State Management

To use useReducer, you first need to define a reducer function. The reducer function is a function that takes the current state and an action object as arguments, and returns the new state.

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

Once you have defined your reducer function, you can use it in your component with the useReducer hook.

const [state, dispatch] = useReducer(reducer, { count: 0 });
Enter fullscreen mode Exit fullscreen mode

The dispatch function is a function that you can use to send actions to the reducer. To send an action, you simply call the dispatch function with an action object.

dispatch({ type: 'increment' });

Enter fullscreen mode Exit fullscreen mode

The reducer will then update the state based on the action object that it received.

In the above example, the reducer will increment the count state variable by 1.

Here's an example of how it works put together:


import React, { useReducer } from 'react';

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  function reducer(state, action) {
    switch (action.type) {
      case 'increment':
        return { count: state.count + 1 };
      case 'decrement':
        return { count: state.count - 1 };
      default:
        return state;
    }
  }

  return (
    <div>
      <p>Current count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
    </div>
  );
}

export default Counter;
Enter fullscreen mode Exit fullscreen mode

In this example, we use useReducer to manage the count state variable. We define a reducer function that increments or decrements the count state variable based on the action object that it receives.
We then use the useReducer hook in our Counter component to get the current state and the dispatch function. We use the dispatch function to send actions to the reducer when the user clicks on the increment or decrement buttons.

Conclusion

React Hooks have significantly improved the way we develop React components. They make the process more elegant and enjoyable. As you begin to build React applications, keep in mind that React Hooks are dependable tools that can make complex state and effect management a breeze. They efficiently replace the need for class components, guiding you as you work with functional components, now elevated by the innovation of React Hooks.

Top comments (0)