React Hooks : Easy and Helpful guide to Modern React State Management

In this article, we’ll learn about React Hooks.

React has made a name for itself as a powerhouse for creating dynamic and interactive user interfaces in the realm of contemporary web development. The advent of React Hooks has given developers a strong toolkit for controlling state and side effects, streamlining and organizing the development process.

What are React Hooks:

React Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class.

Prior to React Hooks, class components and lifecycle methods were the main tools used to manage stateful functionality in React components.

However, as applications expanded in size and complexity, this method can result in complex and difficult-to-maintain code. React Hooks were added in version 16.8 to allow stateful code to be contained and reused in functional components.

React state and lifecycle aspects can be “hooked into” from function component code via hooks. They provide a clearer, more logical approach to deal with state, effects, context, and other things.

Understand Core React Hooks

1). useState

Perhaps the most basic hook is the useState one. Without the requirement for a class, it enables you to add state to your functional components. By declaring a state variable and its initial value with useState, the hook will return the variable’s current value along with a function to update it.

to use it, you need to import it at the top of the file. see below example :

import { useState } from 'react';
export default function MyInput() {
const[text, setText] =useState('hello');
functionhandleChange(e){
setText(e.target.value);
}

return(
<>
<input value={text} onChange={handleChange} />
<p>You typed: {text}</p>
<button onClick={() => setText('hello')}>
Reset
</button>
</>
);
}

In the above example, it will display “Hello” text by default. when the user types any text, it will change and display.

2). useEffect

Lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount are replaced by the useEffect hook. It gives you the ability to carry out side effects within your components, like data fetching, DOM manipulation, and subscriptions.

Some components need to stay connected to the network, some browser API, or a third-party library, while they are displayed on the page. These systems aren’t controlled by React, so they are called external.

Think of a React component like a small part of your web page that can change over time. The useEffect hook is like a helper that lets you tell your component, “Hey, when something specific happens, I want you to do this.”

To connect your component to some external system, call useEffect the top level of your component:

to use it, you need to import it at the top of the file. see below example :

import { useState, useEffect } from "react";
function CountTimer() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setTimeout(() => {
      setCount((count) => count + 1);
    }, 1000);
  });
  return <h1>Rendered {count} times</h1>;
}
export default CountTimer;

In the above example, it will increment and display count every second.

useState is a hook used to manage state in functional components, while useEffect is a hook used to manage side effects (like fetching data, setting up event listeners, or updating the DOM) in functional components.

Additional Built-in React Hooks:

1). useContext

The useContext hook is used to access the context values that are provided by a parent component higher up in the component tree, without having to pass those values through each level of the tree explicitly as props.

Context provides a way to share data that can be considered “global” for a tree of React components.

see below for an example:

import React, { useContext } from 'react';

// Create a context
const ThemeContext = React.createContext();

// Create a provider component
function ThemeProvider({ children }) {
  const theme = 'light'; // You can replace this with a state or dynamic value
  return (
    <ThemeContext.Provider value={theme}>
      {children}
    </ThemeContext.Provider>
  );
}

// A child component that uses the context value

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ backgroundColor: theme === 'light' ? 'white' : 'black', color: theme === 'light' ? 'black' : 'white' }}>
      Themed Button
    </button>
  );
}

export function App() {
  return (
   <ThemeProvider>
      <div>
        <h1>Theme App</h1>
        <ThemedButton />
      </div>
    </ThemeProvider>
  );
}
In this example, the ThemeContext is created using React.createContext(), and then the ThemeProvider component provides the theme value to its child components through the context. The ThemedButton component uses the useContext hook to access the theme value directly, avoiding the need to pass it as a prop from the top-level App component.

 

2). useReducer

The useReducer hook lets you handle state similarly to useState, but it’s more suited for sophisticated state logic where state transitions could depend on the state that came before.

. It’s often used when the state transitions involve multiple actions and a predictable pattern, resembling the way state management works in Redux.

The useReducer hook takes in a reducer function and an initial state, and returns the current state and a dispatch function. The reducer function takes the current state and an action as arguments and returns the new state based on the action.

see below for an example:

import React, { useReducer } from 'react';

// Initial state
const initialState = { count: 0 };

// Reducer function
const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }

};

function Counter() {

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

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
    </div>
  );
}

export function App() {
  return (
   <div>
      <h1>Counter App</h1>
      <Counter />
    </div>
  );
}

In this example, the reducer function handles the state transitions based on the action type. The useReducer hook is used in the Counter component to manage the state. When the “Increment” button is clicked, it dispatches an action with the type ‘INCREMENT’, which causes the state to be updated and the component to re-render.

useReducer is useful in scenarios where the state transitions are more complex and involve multiple actions, and you want to encapsulate the logic within a reducer function. It’s especially handy when you need to manage state transitions involving previous state values or when you want to organize state logic separately from the component itself.

3). useMemo and useCallback

i). useMemo

useMemo is used to memoize a value, which means it will only recompute the value if the dependencies have changed. This is particularly useful when you have a costly computation that you don’t want to repeat on every render.

see below example:

import React, { useMemo, useState } from 'react';
function ExpensiveComponent({ data }) {
  const expensiveValue = useMemo(() => {
    let result = 0;
    for (const item of data) {
      result += item;
    }
    return result;
  }, [data]);
  return <div>Expensive Value: {expensiveValue}</div>;
}

export default App;

export function App() {
    const [data, setData] = useState([1, 2, 3, 4, 5]);
  return (
   <div>
      <button onClick={() => setData([...data, Math.random()])}>
        Add Random Number
      </button>
      <ExpensiveComponent data={data} />
    </div>
  );
}

 

In this example, the expensiveValue will only be recalculated when the data prop changes. If other props or state within the ExpensiveComponent change, the memoized value won’t be recomputed.

ii). useCallback

useCallback is used to memoize a function. It’s particularly useful when you want to pass functions down to child components as props, but you want to ensure that the function reference remains the same between renders unless its dependencies change.

see below example:

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

function ChildComponent({ onClick }) {
  return <button onClick={onClick}>Click Me</button>;
}

function ParentComponent() {
  const [count, setCount] = useState(0);
 
 // Memoized function using useCallback
  const handleClick = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

export function App() {
    const [data, setData] = useState([1, 2, 3, 4, 5]);
  return (
   <ParentComponent />
  );
}

In this example, the handleClick function is memoized using useCallback. This ensures that even though handleClick is defined within ParentComponent and uses the count state, it won’t be recreated on every render of ParentComponent, because it will only update when the count dependency changes.

Both useMemo and useCallback help improve performance by preventing unnecessary recalculations and unnecessary re-renders of child components.

Both React hooks improve efficiency by reserving values and functions in memory. UseMemo caches a computation’s result, whereas useCallback caches a function instance; both are particularly helpful for reducing the number of times a costly calculation needs to be rendered or for avoiding wasteful re-rendering.

4). Custom React Hooks

A custom hook is a function that allows you to encapsulate and reuse logic that can be shared across different components. Custom React hooks enable you to abstract away complex logic and state management, making your components more modular and easier to maintain.

The flexibility to design unique hooks is one of React Hooks’ strongest features. A JavaScript function called a custom hook makes use of one or more built-in React hooks to offer a particular piece of functionality. This encourages code reuse and effectively isolates concerns.

see below example:

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

// Custom hook to get window dimensions
function useWindowDimensions() {
  const [windowDimensions, setWindowDimensions] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    function handleResize() {
      setWindowDimensions({
        width: window.innerWidth,
        height: window.innerHeight
      });
    }

    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return windowDimensions;
}

export function App() {
  const { width, height } = useWindowDimensions();
  return (
    <div>
      <h1>Window Dimensions</h1>
      <p>Width: {width}px</p>
      <p>Height: {height}px</p>
    </div>
  );
}

In this example, the useWindowDimensions custom hook encapsulates the logic to track and update the window dimensions. It returns the current dimensions, and the App component uses this custom hook to display the window width and height.

Custom React hooks are powerful because they allow you to separate concerns, promote reusability, and improve the readability of your code. You can create custom React hooks for a wide variety of purposes, such as data fetching, form handling, state management, authentication, and more.

Benefits of React Hooks

  • Readability and Reusability: React Hooks encourage a more linear and cleaner code structure. They enable developers to write smaller, more focused functions that can be reused across components.
  • Simpler State Management: With useState and useReducer, state management becomes less convoluted, eliminating the need for complex class structures and reducing the chance of bugs.
  • Easier Testing: Functional components with hooks are easier to test, as they are more self-contained and don’t require mocking of lifecycle methods.

Use cases:

  • Managing Component State: useState simplifies state management, making it easier to handle user interactions and component re-renders.
  • Fetching Data: useEffect allows data fetching without resorting to lifecycle methods, enabling a more straightforward approach to asynchronous operations.
  • Global State Management: useContext and useReducer facilitate global state sharing and management in a more organized manner.
  • Code Reusability: Custom hooks enable developers to encapsulate specific functionality (e.g., form handling, authentication) and reuse it across different components.

 

Conclusion:

React Hooks, which provide a more elegant and structured way to manage state and side effects, have caused a paradigm change in React development.

Developers now have access to a robust toolbox, ranging from fundamental hooks like useState and useEffect to more sophisticated use cases and best practices.

Developers may construct cleaner, more maintainable codebases that lead to quicker development cycles, higher performance, and improved user experiences by embracing React Hooks and comprehending their subtleties.

React Hooks will likely stay a crucial and fascinating part of contemporary front-end programming as the React ecosystem develops.

References:

I hope this blog post has given you a better understanding of React Hooks. If you have any questions, please feel free to leave a comment below.