Matthew Tyson
Contributing writer

8 more React hooks you need to know about

feature
12 Jul 202312 mins
Development ToolsJavaScriptSoftware Development

useState is the most well-known hook for using functional components in React, but it's just a start. Here are eight more React hooks, and how to use them.

A fishing lure with multiple hooks baits a binary stream. [fraud / phishing / social engineering]
Credit: Curaga / Cofotoisme / Getty Images

React remains the pacesetter among JavaScript UI frameworks. There are plenty of ongoing developments in React, but the most important shift of the last few years was the move to functional components. Functional components rely on hooks for many of their capabilities. The most common hook is useState, but there are many others.

Here’s a look at eight useful React hooks you may not know about, and how to use them.

useReducer

Everyone knows about useState because it replaces an essential feature of class-based components—the member variables to hold state—with a functional equivalent. The useReducer hook does something similar, but for more complex scenarios where state transitions are more involved and the application benefits from making transitions explicit. The useReducer hook is inspired by the reducers found in Redux. It can be seen as a middle ground between the simplicity of useState and the complexity of a state management system like Redux.

Here’s an example of how to work with the useReducer hook. You can also see the reducer live in this JSFiddle.

Listing 1. Simple useReducer example

 

const {useReducer, Fragment} = React;


const initialState = {
  text: "",
  isUpperCase: false,
};

const reducer = (state, action) => {
  switch (action.type) {
    case 'SET_TEXT':
      return {
        ...state,
        text: action.text,
      };
    case 'TOGGLE_UPPER_CASE':
      return {
        ...state,
        text: state.text.toUpperCase(),
      };
    case 'LOWERCASE':
      return {
        ...state,
        text: state.text.toLowerCase(),
      };
    default:
      return state;
  }
};

const App = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const handleChange = (event) => {
    dispatch({ type: 'SET_TEXT', text: event.target.value });
  };

  const handleToggleUpperCase = () => {
    dispatch({ type: 'TOGGLE_UPPER_CASE' });
  };

  const handleLowerCase = () => {
    dispatch({ type: 'LOWERCASE' });
  };

  return (
    <div>
      <input type="text" value={state.text} onChange={handleChange} />
      <button onClick={handleToggleUpperCase}>Toggle Upper Case</button>
      <button onClick={handleLowerCase}>Toggle Lower Case</button>
      

{state.text}

</div> ); };

The purpose of this example is to take the text from the input box and let the user click buttons to display the text in all uppercase or all lowercase. The code declares a new reducer with const [state, dispatch] = useReducer(reducer, initialState);. The useReducer takes the reducer function and the initialstate and returns an array, which we then destructure to state and dispatch variables. 

The reducer itself is defined with: const reducer = (state, action) =>, giving a two-argument function. Whenever the dispatch function is called in the code, it will pass the current state along with an action object. In this case, the action object has a type field and we use that to determine how to mutate the state. 

In a moderately complex application, useReducer can be helpful in managing complexity, and can even be shared across the application using the context. When useState is difficult to manage because of the complexity of the application, the useReducer hook can help.

useCallback

The useCallback hook is a performance hook. It takes a function and ensures that only a single version will be returned and reused for all callers. If the function is expensive and called repeatedly by a loop or child components, the useCallback hook can net significant performance gains. This kind of optimization is known as memoizing a function.

In Listing 2, we have an example of using useCallback to use the same function across many items in a list. Here’s the example in a live JSFiddle.

Listing 2. useCallback in a iterated list

 

const List = ({ items }) => {
  const [counter, setCounter] = React.useState(0);

  const incrementCounter = React.useCallback(() => {
    setCounter(counter + 1);
  }, [counter]);

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          {item} - {counter}
          <button onClick={incrementCounter}>Increment</button>
        </li>
      ))}
    </ul>
  );
};

We use React.useCallback() to create a new memoized function at incrementCounter. We can use the memoized function as a normal function in the onClick handler, in the list. useCallback() takes a function as a first argument. Within that function, we can perform any work we need. The key difference is that React simply returns the cached value of the function unless something has changed in the list of dependency variables, which in our example is the counter variable. 

This is a precious magic power in cases where you need to share an expensive function among several callers, especially child components. Bear in mind as we look at the next hook (useMemo) that useCallback stashes the function itself. That is to say, useCallback prevents the actual function from being recreated each time it appears, and only recreates it when necessary.

useMemo

The useMemo hook is useCallback’s sibling. Where useCallback caches the function, useMemo caches the function return value. It’s a subtle distinction, but important. 

When should you use useMemo versus useCallback?  The answer is: use useMemo when you can, and useCallback when you have to. The useCallback hook is merited when the performance hit you are avoiding is the creation of the function itself in the rendering, while useMemo will not prevent the function from being recreated wherever it appears. However, useMemo will ensure the function returns a cached value if the dependencies have not changed.

Listing 3 shows useMemo in an example. ,You can also see this example in a live JSFiddle.

Listing 3. useMemo

 

const { useMemo, useState } = React;

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

  const computeExpensiveValue = (count) => {
    let result = 0;
    for (let i = 0; i < count * 10000000; i++) {
      result += i;
    }
    return result;
  };

  const memoizedValue = useMemo(() => computeExpensiveValue(count), [count]);

  return (
    <div>
      

Count: {count}

Expensive Value: {memoizedValue}

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

In this example, we have a function that costs a lot to compute: computeExpensiveValue. It depends on a single input, count. We can use computeExpensiveValue(count), [count]) to tell react: only run this function if the count has changed; otherwise, return the cached computation.

Again, the difference with useCallback is not obvious. The important distinction is: useCallback is the hook to use when the function itself is being repeatedly instantiated, incurring a performance hit. Otherwise, useMemo is the better choice.

useContext

In React, the context is a variable scope that exists outside of the components and to which all components have access. As such, it is a quick and easy global space for application-wide data. For complex scenarios, it might be better to use an official data store like Redux, but for many uses, context will suffice. The useContext hook is how functional components interact with the context.

In Listing 4, we have two components, Speak and Happy, that are used by the application parent component, App. The user can toggle between a dog and a cat state, and via the global context, the child components will reflect the choice (wagging tail versus purring, for example). You can also check out the live JSFiddle for this example.

Listing 4. useContext in action

 

const { createContext, useContext, useState } = React;

const AnimalContext = createContext();

const Speak = () => {
  const { animalType } = useContext(AnimalContext);

  return (
    <div>
      {animalType === 'dog' ? 'woof' : 'meow'}
    </div>
  );
};

const Happy = () => {
  const { animalType } = useContext(AnimalContext);

  return (
    <div>
      {animalType === 'dog' ? 'wag tail' : 'purr'}
    </div>
  );
};

const App = () => {
  const [animalType, setAnimalType] = useState('dog');

  const toggleAnimalType = () => {
    setAnimalType(prevAnimalType => (prevAnimalType === 'dog' ? 'cat' : 'dog'));
  };

  return (
    <div>
      <button onClick={toggleAnimalType}>Toggle animal type</button>
      <div>{animalType}</div>
      <AnimalContext.Provider value={{ animalType }}>
        <Speak />
        <Happy />
      </AnimalContext.Provider>
    </div>
  );
};

useRef

The useRef hook lets you manage a reference outside the render cycle. useState causes the React engine to render when it changes, whereas useRef does not. The useRef hook is like a special area off to the side of React that says: This variable is special and it isn’t part of the reactive UI. The most common use case for useRef is to gain access directly to the DOM and its API. Normally, in reactive thinking, this is avoided and everything should be done through the reactive engine, but sometimes it’s unavoidable.

In Listing 5, when the button is clicked on, we use the useRef hook to hold a reference to the input field and use the DOM methods to put the focus on it and set its value to “Something amazing.” Here’s the JSFiddle for the useRef example.

Listing 5. Simple useRef example

 

const App = () => {
  const inputRef = React.useRef(null);

  const handleClick = () => {
    inputRef.current.focus();
    inputRef.current.value="Something amazing";
  };

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

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <App/>
);

useEffect

useEffect is the second most common hook after useState. It is frequently used to make API calls, change the DOM, or take other action (that is, cause an effect) when something changes in the component state. In a sense, useEffect lets you define a reactive variable or variables and the behavior that will occur for them. 

In Listing 6, we define a drop-down list to select a Star Wars character. When this value changes, we make an API call to the Star Wars API (SWAPI) and display the character’s data. Here’s the live JSFiddle for this example.

Listing 6. useEffect with API call

 

const { useState, useEffect } = React;

const StarWarsCharacter = () => {
  const [character, setCharacter] = useState('Luke Skywalker');
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(`https://swapi.dev/api/people/?search=${character}`);
        const jsonData = await response.json();
        setData(jsonData.results[0]);
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    };

    fetchData();
  }, [character]);

  const handleCharacterChange = (event) => {
    setCharacter(event.target.value);
  };

  return (
    <div>
      <h1>Star Wars Character Details</h1>
      <select value={character} onChange={handleCharacterChange}>
        <option value="Luke Skywalker">Luke Skywalker</option>
        <option value="Han Solo">Han Solo</option>
        <option value="Princess Leia">Princess Leia</option>
      </select>
      {data ? (
        <div>
          <h2>{data.name}</h2>
          

Height: {data.height}

Mass: {data.mass}

Hair Color: {data.hair_color}

</div> ) : (

Loading...

)} </div> ); };

useLayoutEffect

useLayoutEffect is a lesser known hook that comes into play when you need to make measurements of the rendered DOM. The useLayoutEffect hook is called after React draws the UI, so you can count on it giving access to the actual layout. 

In Listing 7, we use useRef to grab an element off the DOM and useLayoutEffect to be notified when the rendering is complete. Then, we calculate the element size using the DOM API. Also see the JSFiddle for this example.

Listing 7. useLayoutEffect to measure an element

 

const { useState, useRef, useLayoutEffect } = React;

const App = () => {
  const [width, setWidth] = useState(null);
  const elementRef = useRef(null);

  useLayoutEffect(() => {
    if (elementRef.current) {
      const { width } = elementRef.current.getBoundingClientRect();
      setWidth(width);
    }
  }, []);

  return (
    <div>
      <div ref={elementRef}>Measure Width</div>
      

Width: {width}

</div> ); };

useImperativeHandle

Sometimes, you need to get the reference directly to a component. You can do this with useRef, and if you also want to provide access to the component’s DOM, you can use forwardRef. Sometimes, though, you need to customize the behavior that the component exposes via the reference. For that, you need the useImperativeHandle hook. 

For this one, the example is worth a thousand words. Listing 8 (live here) shows a parent component, App, with a child component, Expounder. Expounder exposes a customized ref API with useImperativeHandle, such that App can call expound() on the reference, causing the customized behavior (outputting “The way that can be told is not the eternal way” to the UI).

Note that the ChildComponent in Listing 8 uses forwardRef to include the DOM ref.

Listing 8. useImperativeHandle

 

const { useState, useRef, forwardRef, useImperativeHandle } = React;

const ChildComponent = forwardRef((props, ref) => {
  const [count, setCount] = useState("");

  const expound = () => {
    setCount("The way that can be told is not the eternal way.");
  };

  useImperativeHandle(ref, () => ({
    expound
  }));

  return (
    <div>
      

Count: {count}

</div> ); }); const App = () => { const childRef = useRef(null); const handleClick = () => { childRef.current.expound(); }; return ( <div> <ChildComponent ref={childRef} /> <button onClick={handleClick}>Expound</button> </div> ); };

So, useImperativeHandle lets you take the first argument, the original ref, and then decorate it with whatever object is returned by the second argument function. In this case, we use a destructuring assignment to create an anonymous inline object with just an expound method on it: useImperativeHandle(ref, () => ({ expound }));.

The result of using the useImperativeHandle hook is that the reference the ParentComponent receives from ChildComponent also has an expound method on it, which it can call to execute the necessary functionality.

Conclusion

Using a broader palette of hooks available to React is an important aspect of leveraging the full power of the framework. You’ve seen a good sampling of some useful ones here, and React ships with even more. Beyond that, third-party hooks are available for various purposes and for integrating with frameworks. Finally, it’s possible to define your own custom hooks if the need arises.

Exit mobile version