DEV Community

Cover image for Exploring lesser-known React hooks and their importance
Gbadebo ifedayo
Gbadebo ifedayo

Posted on • Edited on

Exploring lesser-known React hooks and their importance

Hooks which were introduced in React version 16.8, provide function components access to React's state and lifecycle features. Hooks make components more organized and reusable by enabling developers to divide components into smaller parts that can be joined easily.
The most commonly used hooks which are useState and useEffect are useful in React apps as useState allows developers to add state to functional components, while useEffect helps manage side effects such as data fetching and DOM updates. Both hooks can be combined to carry out complex tasks.
Although these fundamental hooks are necessary for creating React applications, this article focuses on uncovering lesser-known React hooks that offer specialized solutions for improving performance and simplifying code.

useContext

The useContext hook is a fundamental part of React that provides a better way to manage state globally. It provides a way to share state across multiple nested components without needing to pass props through every level of the component tree.

Use case: counter app

When a state is passed down nested components without the use of the useContext hook like in the example below, it results in what is called "prop drilling".

import React, { useState } from 'react';

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

  return (
    <div>
      <h1>Counter</h1>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
      <ParentComponent count={count}/>
    </div>
  );
};

function Counterchild ({ count }) {
  return (
    <div>
      <h2>Counter Child</h2>
      <ChildComponent count={count}/>
    </div>
  );
};

function Counterdisplay ({ count }) {
  return (
    <div>
      <h3>Counter Display</h3>
      <p>Count: {count}</p>
    </div>
  );
};

export default Counter;

Enter fullscreen mode Exit fullscreen mode

In this example, the count state is defined in the Counter component, and it's passed down as props through the Counterchild to the Counterdisplay. Each component receives the count state as a prop, allowing them to display it's value.
Although this method is effective, it becomes lengthy when the state is passed down through many nested child components.
To use the useContext hook, we first import createContext and initialize it.

import { useState, createContext } from "react";

const count = createContext()
Enter fullscreen mode Exit fullscreen mode

Next, we'll wrap the tree of components that require the state with the context provider.

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

  return (
<UserContext.Provider value={user}>
    <div>
      <h1>Counter</h1>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
      <Counterchild count={count}/>
    </div>
</UserContext.Provider>
  );
};

Enter fullscreen mode Exit fullscreen mode

To make use of the previously created context located in the parent component, we need to import useContext.

import { useContext } from "react";
Enter fullscreen mode Exit fullscreen mode

Now, our context is available for use in the child component.

function Counterdisplay() {
  const user = useContext(count);

  return (
    <div>
      <h3>Counter Display</h3>
      <p>Count: {count}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

useRef

The useRef hook allows us to create a mutable reference to a value or a DOM element. Unlike the useState hook, useRef doesn't cause a re-render when its value changes.
useRef accepts one argument as the initial value and returns one object called current. For example:

const initialValue = ''
const reference = useRef(initialValue)
Enter fullscreen mode Exit fullscreen mode

A reference can be accessed and changed using the current object:

  useEffect(() => {
    reference.current = 'My Reference';
  });
Enter fullscreen mode Exit fullscreen mode

Use case 1: Tracking application re-renders

We would start an infinite loop if we attempted to count how many times our application renders using the useState hook as it causes a re-render itself. To avoid this, we can use the useRef Hook:

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

function App() {
  const [inputValue, setInputValue] = useState("");
  const count = useRef(0);

  useEffect(() => {
    count.current = count.current + 1;
  });

  return (
    <>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <h1>Render Count: {count.current}</h1>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the example above we set the initial value of the reference with useRef(0) and access the value of count using count.current.

Use case 2: Focusing the input element

useRef can be used to hold a reference to a DOM element, we can then directly access and perform functions on this element by carrying out the following:

  1. Define the reference to be used in accessing the element

  2. Assign the reference to the ref attribute of the element to be interacted with

import { useRef } from "react";

function App() {
  const inputElement = useRef();

  const focusInput = () => {
    inputElement.current.focus();
  };

  return (
    <>
      <input type="text" ref={inputElement} />
      <button onClick={focusInput}>Focus Input</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the example above, the reference that was created was assigned to the input tag, and we were able to focus the element using the reference assigned to it.

Limitations

Updating a reference should be done inside a useEffect callback or inside handlers (event handlers, timer handlers, etc.), not inside the immediate scope of the functional component's function, as the function scope of the functional component should either calculate the output or invoke hooks.

import { useRef } from "react";

function App() {
  const count = useRef(0);
      useEffect(() => {
    count.current++; // Success

    setTimeout(() => {
      count.current++; // Success
    }, 1000);
  }, []);

  count.current++; // Error
}

Enter fullscreen mode Exit fullscreen mode

useReducer

The useReducer hook is used for managing complex state logic in a more organized way. It's an alternative to useState and is particularly useful when state changes entail numerous actions.
useReducer takes in a reducer function and an initial state. It returns the current state and a dispatch function that you can use to send actions to update the state based on the logic defined in the reducer function

Use case: Creating a counter

import { useReducer } from 'react';

// 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() {
  const initialState = { count: 0 };
  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 default Counter;
Enter fullscreen mode Exit fullscreen mode

In this example, the reducer function handles different actions ('INCREMENT' and 'DECREMENT') to update the state. The useReducer hook initializes the state with { count: 0 } and provides a dispatch function to trigger these actions.
When we click the "Increment" button, it dispatches an action of type 'INCREMENT', which causes the state to be updated and the count to increase. Similarly, clicking the "Decrement" button dispatches an action of type 'DECREMENT', reducing the count.

useCallback

The useCallback hook is primarily used to memoize functions, preventing their needless re-creation on each render. This can be very helpful when sending functions to child components as props.

Use case: Improving performance

The useCallback's basic syntax is as follows:

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

function ChildComponent({ handleClick }) {
  console.log('ChildComponent rendered');
  return <button onClick={handleClick}>Click me</button>;
}

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

  // Using useCallback to memoize the function
  const increment = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  console.log('ParentComponent rendered');

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


Enter fullscreen mode Exit fullscreen mode

The increment function is memoized because the useCallback hook ensures the function remains the same between renders if the dependencies (count in this case) haven't changed. This prevents the unnecessary re-creation of the increment function when the ParentComponent re-renders.
The increment function is then passed as the handleClick prop to the ChildComponent. Thanks to useCallback, the handleClick prop will remain stable between renders, even as the ParentComponent updates its state.

useMemo

The useMemo and useCallback hooks have some similarities. The main difference is useMemo returns a memoized value whereas useCallback returns a memoized function.

Use case: Improving performance

The useMemo Hook can be used to keep expensive, resource intensive functions from needlessly running, thereby improving the performance of the React app.

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

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

  // Using useMemo to optimize a calculation
  const squaredCount = useMemo(() => {
    console.log('Calculating squaredCount');
    return count * count;
  }, [count]);

  console.log('ParentComponent rendered');

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

export default ParentComponent;

Enter fullscreen mode Exit fullscreen mode

In this example, the useMemo hook is used to optimize the calculation of the squaredCount value and the calculation depends on the count state. By specifying [count] as the dependency array, we ensure that the calculation is only recomputed when the count state changes.

When you click the "Increment" button, the count state is updated, triggering a re-render of the ParentComponent. However, thanks to the useMemo optimization, the calculation of squaredCount only occurs when the count state changes, avoiding unnecessary recalculations.

Conclusion

React hooks are crucial tools for front-end development for creating dynamic and effective applications. This article delves into lesser-known hooks like "useContext," "useRef," "useReducer," "useCallback," and "useMemo" that provide solutions for various challenges. I urge you to experiment with and include these hooks in your projects to attain a better understanding of React's capabilities.

Resources

Top comments (0)