React Hooks: The Deep Cuts

Avatar of Blessing Ene Anyebe
Blessing Ene Anyebe on (Updated on )

Hooks are reusable functions. They allow you to use state and other features (e.g. lifecycle methods and so on) without writing a class. Hook functions let us “hook into” the React state lifecycle using functional components, allowing us to manipulate the state of our functional components without needing to convert them to class components.

React introduced hooks back in version 16.8 and has been adding more ever since. Some are more used and popular than others, like useEffect, useState, and useContext hooks. I have no doubt that you’ve reached for those if you work with React.

But what I’m interested in are the lesser-known React hooks. While all React hooks are interesting in their own way, there are five of them that I really want to show you because they may not pop up in your everyday work — or maybe they do and knowing them gives you some extra superpowers.

useReducer

The useReducer hook is a state management tool like other hooks. Specifically, it is an alternative to the useState hook.

If you use the useReducer hook to change two or more states (or actions), you won’t have to manipulate those states individually. The hook keeps track of all the states and collectively manages them. In other words: it manages and re-renders state changes. Unlike the useState hook, useReducer is easier when it comes to handling many states in complex projects.

Use cases

useReducer can help reduce the complexity of working with multiple states. Use it when you find yourself needing to track multiple states collectively, as it allows you to treat state management and the rendering logic of a component as separate concerns.

Syntax

useReducer accepts three arguments, one of which is optional:

  • a reducer function
  • initialState
  • an init function (optional)
const [state, dispatch] = useReducer(reducer, initialState)
const [state, dispatch] = useReducer(reducer, initialState initFunction) // in the case where you initialize with the optional 3rd argument

Example

The following example is an interface that contains a text input, counter, and button. Interacting with each element updates the state. Notice how useReducer allows us to define multiple cases at once rather than setting them up individually.

import { useReducer } from 'react';
const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    case 'USER_INPUT':
      return { ...state, userInput: action.payload };
    case 'TOGGLE_COLOR':
      return { ...state, color: !state.color };
    default:
      throw new Error();
  }
}

function App() {
  const [state, dispatch] = useReducer(reducer, { count: 0, userInput: '', color: false })

  return (
    <main className="App, App-header" style={{ color: state.color ? '#000' : '#FF07FF'}}>
      <input style={{margin: '2rem'}}
        type="text"
        value={state.userInput}
        onChange={(e) => dispatch({ type: 'USER_INPUT', payload: e.target.value })}
      />
      <br /><br />
      <p style={{margin: '2rem'}} >{state.count}</p>
      <section style={{margin: '2rem'}}>
        <button  onClick={(() => dispatch({ type: 'DECREMENT' }))}>-</button>
        <button onClick={(() => dispatch({ type: 'INCREMENT' }))}>+</button>
        <button onClick={(() => dispatch({ type: 'TOGGLE_COLOR' }))}>Color</button>
      </section>
      <br /><br />
      <p style={{margin: '2rem'}}>{state.userInput}</p>
    </main>
  );
}
export default App;

From the code above, noticed how we are able to easily managed several states in the reducer (switch-case), this shows the benefit of the useReducer. This is the power it gives when working in complex applications with multiple states.

useRef

The useRef hook is used to create refs on elements in order to access the DOM. But more than that, it returns an object with a .current property that can be used throughout a component’s entire lifecycle, allowing data to persist without causing a re-render. So, the useRef value stays the same between renders; updating the reference does not trigger a re-render.

Use cases

Reach for the useRef hook when you want to:

  • Manipulate the DOM with stored mutable information.
  • Access information from child components (nested elements).
  • Set focus on an element.

It’s most useful when storing mutatable data in your app without causing a re-render.

Syntax

useRef only accepts one argument, which is the initial value.

const newRefComponent = useRef(initialValue);

Example

Here I used the useRef and useState hook to show the amount of times an application renders an updated state when typing in a text input.

import './App.css'

function App() {
  const [anyInput, setAnyInput] = useState(" ");
  const showRender = useRef(0);
  const randomInput = useRef();
  const toggleChange = (e) => {
    setAnyInput (e.target.value);
    showRender.current++;
  
  }
  const focusRandomInput = () => {
    randomInput.current.focus();
  }

  return (
    <div className="App">
      <input className="TextBox" 
        ref ={randomInput} type="text" value={anyInput} onChange={toggleChange}
      />
      <h3>Amount Of Renders: {showRender.current}</h3>
      <button onClick={focusRandomInput}>Click To Focus On Input </button>
    </div>
  );
}

export default App;

Notice how typing each character in the text field updates the app’s state, but never triggers a complete re-render.

useImperativeHandle

You know how a child component has access to call functions passed down to them from the parent component? Parents pass those down via props, but that transfer is “unidirectional” in the sense that the parent is unable to call a function that’s in the child.

Well, useImperativeHandle makes it possible for a parent to access a child component’s functions.

How does that work?

  • A function is defined in the child component.
  • A ref is added in the parent.
  • We use forwardRef, allowing the ref that was defined to be passed to the child.
  • useImperativeHandle exposes the child’s functions via the ref.

Use cases

useImperativeHandle works well when you want a parent component to be affected by changes in the child. So, things like a changed focus, incrementing and decrementing, and blurred elements may be situations where you find yourself reaching for this hook so the parent can be updated accordingly.

Syntax

useImperativeHandle (ref, createHandle, [dependencies])

Example

In this example, we have two buttons, one that’s in a parent component and one that’s in a child. Clicking on the parent button retrieves data from the child, allowing us to manipulate the parent component. It’s set up so that clicking the child button does not pass anything from the parent component to the child to help illustrate how we are passing things in the opposite direction.

// Parent component
import React, { useRef } from "react";
import ChildComponent from "./childComponent";
import './App.css';

function useImperativeHandle() {
  const controlRef = useRef(null);
  return (
    onClick={
      () => {
        controlRef.current.controlPrint();
      }
    }
    >
    Parent Box
  );
}
export default useImperativeHandle;
// Child component
import React, { forwardRef, useImperativeHandle, useState } from "react";

const ChildComponent = forwardRef((props, ref) => {
  const [print, setPrint] = useState(false);
  useImperativeHandle(ref, () => ({
    controlPrint() 
    { setPrint(!print); },
  })
  );

  return (
    <>
    Child Box
    { print && I am from the child component }
  );
});

export default ChildComponent;

Output

useMemo

useMemo is one of the least-used but most interesting React hooks. It can improve performance and decrease latency, particularly on large computations in your app. How so? Every time a component’s state updates and components re-render, the useMemo hook prevents React from having to recalculate values.

You see, functions respond to state changes. The useMemo hook takes a function and returns the return value of that function. It caches that value to prevent spending additional effort re-rendering it, then returns it when one of the dependencies has changed.

This process is called memoization and it’s what helps to boost performance by remembering the value from a previous request so it can be used again without repeating all that math.

Use cases

The best use cases are going to be any time you’re working with heavy calculations where you want to store the value and use it on subsequent state changes. It can be a nice performance win, but using it too much can have the exact opposite effect by hogging your app’s memory.

Syntax

useMemo( () => 
  { // Code goes here },
  []
)

Example

When clicking the button, this mini-program indicates when a number is even or odd, then squares the value. I added lots of zeros to the loop to increase its computation power. It returns the value in spilt seconds and still works well due to the useMemo hook.

// UseMemo.js
import React, { useState, useMemo } from 'react'

function Memo() {
  const [memoOne, setMemoOne] = useState(0);
  const incrementMemoOne = () => { setMemoOne(memoOne + 1) }
  const isEven = useMemo(() => { 
    let i = 0 while (i < 2000000000) i++ return memoOne % 2 === 0
  },
  [memoOne]);
  
  const square = useMemo(()=> { 
    console.log("squared the number"); for(var i=0; i < 200000000; i++);
    return memoOne * memoOne;
  },
  [memoOne]);

  return (
    Memo One - 
    { memoOne }
    { isEven ? 'Even' : 'Odd' } { square } 
  );
}
export default Memo

Output

useMemo is a little like the useCallback hook, but the difference is that useMemo can store a memorized value from a function, where useCallback stores the memorized function itself.

useCallback

The useCallback hook is another interesting one and the last section was sort of a spoiler alert for what it does.

As we just saw, useCallback works like the useMemo hook in that they both use memoization to cache something for later use. While useMemo stores a function’s calculation as a cached value, useCallback stores and returns a function.

Use cases

Like useMemo, useCallback is a nice performance optimization in that it stores and returns a memoized callback and any of its dependencies without a re-render.

Syntax

const getMemoizedCallback = useCallback (
  () => { doSomething () }, []
);

Example


{ useCallback, useState } from "react";
import CallbackChild from "./UseCallback-Child";
import "./App.css"

export default function App() {
  const [toggle, setToggle] = useState(false);
  const [data, setData] = useState("I am a data that would not change at every render, thanks to the useCallback");
  const returnFunction = useCallback(
    (name) => 
    { return data + name; }, [data]
  );
  return (
    onClick={() => {
      setToggle(!toggle);
    }}
    >
    {" "}

    // Click To Toggle
    { toggle && h1. Toggling me no longer affects any function } 
  ); 
}
// The Child component
import React, { useEffect } from "react";

function CallbackChild(
  { returnFunction }
) {
  useEffect(() => 
    { console.log("FUNCTION WAS CALLED"); },
    [returnFunction]);
  return { returnFunction(" Hook!") };
}
export default CallbackChild;

Output

Final thoughts

There we go! We just looked at five super handy React hooks that I think often go overlooked. As with many roundups like this, we’re merely scratching the surface of these hooks. They each have their own nuances and considerations to take into account when you use them. But hopefully you have a nice high-level idea of what they are and when they might be a better fit than another hook you might reach for more often.

The best way to fully understand them is by practice. So I encourage you to practice using these hooks in your application for better understanding. For that, you can get way more in depth by checking out the following resources: