Leia em Português

Explaining all React Hooks with examples

Photo by Oliver Paaske on Unsplash

React Hooks were released two years ago on React version 16.8. It's not so common to see the usage of all of them but they might be very useful to simplify a feature or improve the performance in our application, so, I'd like to explain and give some examples of usage of all React Hooks.

In this post I'll cover all React Hooks:

  • useState.
  • useEffect.
  • useRef.
  • useContext.
  • useReducer.
  • useMemo.
  • useCallback.
  • useDebug.
  • useLayoutEffect.

useState

The simplest and most used of React Hooks, useState allows you to store any value in a function component, like string, object, array, boolean, number, null.

It has the same functionality of this.state in a React Class Component.

You can define its value by passing a value directly or calling a method that accesses the previous value and returns the new value.

import { useState } from "react";

export default function Component() {
  const [counter, setCounter] = useState(0);
  return (
    <>
      <p>Clicks: {counter}</p>
      <button onClick={() => setCounter((prev) => ++prev)}>Increase 1</button>
    </>
  );
}

useEffect

useEffect allows you to call a method during some moments in the component lifecycle:

  • On the first render.
  • When a watched value is updated.
  • When the component is unmounted.

It's similar to the methods componentDidMount, componentDidUpdate and componentWillUnmount in a Class Component.

import { useEffect } from "react";

export default function Component() {
  const fetchSomething = (params) => {};
  const onResizeScreenHandler = () => {};

  useEffect(() => {
    fetchSomething(filters);
  }, [filters]);

  useEffect(() => {
    document.addEventlistener("resize", onResizeScreenHandler);
    return () => document.removeEventlistener("resize", onResizeScreenHandler);
  }, []);
}

To call the function inside useEffect once, let the second parameter, an array, empty.

useEffect(fn, []);

To observe a value, add it in the second parameter.

useEffect(() {
  // Do something with updated x
}, [x]);

The function that you return will be called when the component is unmounted.

useEffect(() => {
  return () => doSomething();
}, []);

useRef

The useRef hook lets you:

  • Access an element on the DOM.
  • Store an immutable value during the component's lifecycle.

Its value is accessed by .current:

const time = useRef(0);
console.log(time.current); // 0;

Accessing an element on the DOM.

import { useRef, useEffect } from "react";

export default function Component() {
  const textRef = useRef(null);
  useEffect(() => {
    if (textRef) {
      const elementWidth = textRef.current.offsetWidth;
    }
  }, [textRef]);

  return <p ref={textRef}>Hello, React Hooks</p>;
}

Delay effect

You can use useRef to create a delay to call a method. For instance, wait x seconds to request something as soon the user finishes typing and not overload your API every time he types on deletes a letter.

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

export default function Form() {
  const [text, setText] = useState("");
  const timeToCallSomething = useRef(null);

  const fetchSomething = () => {
    if (text) {
      // Fetch an API.
    }
  };

  useEffect(() => {
    if (timeToCallSomething.current) {
      clearInterval(timeToCallSomething.current);
    }
    timeToCallSomething.current = setTimeout(fetchSomething, 1000);
    return () => clearInterval(timeToCallSomething.current);
  }, [text]);

  const onChangeHandler = ({ target: { value } }) => {
    setText(value);
  };

  return <input value={text} onChange={onChangeHandler} />;
}

Passing a ref by props

If you want to pass a ref to use in a child component, you need to use the method React.forwardRef in the child component and receive the ref as the second parameter.

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

export default function Parent() {
  const childRef = useRef(null);

  return <Text ref={childRef} />;
}

const Text = forwardRef((props, ref) => {
  return <p ref={ref}>Exemplo de texto</p>;
});

useReducer

It's an alternative to the useState hook and works like the reducer in Redux. It watches a pre-defined type and returns a new state depending on that type.

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

function Counter() {
  const [state, dispatch] = useReducer(reducer, 0);
  return (
    <>
      Count: {state}
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
    </>
  );
}

useContext

The useContext hook allows you to store and access a value anywhere within its hierarchy. It works like Redux, by the way, I already wrote an article showing how to replace Redux by React Hooks.

It's useful when you often use a value and don't want to pass it to the children components by props.

// hooks/useUserContext.js
import { useContext, createContext } from "react";

// Our context
const UserContext = createContext({ name: "" });

// The main component that receives a value and updates our context.
export function UserContextProvider({ name, children }) {
  return <UserContext.Provider value={name}>{children}</UserContext.Provider>;
}

// A custom hook to get the current value.
export default function useUserContext() {
  return useContext(UserContext);
}
// App.js
import { UserContextProvider } from "~hooks/useUserContext";

export default function App() {
  return (
    <UserContextProvider value="John">
      <YourAppTree />
    </UserContextProvider>
  );
}
// YourAppTree.js
import useUserContext from "~hooks/useUserContext";

export default function YourAppTree() {
  const userName = useUserContext(); // Initially the value will be "" and then John.
  return <p>Hi, {userName}</p>;
}

The next React Hooks, useMemo and useCallback, are commonly used to solve performance issues or improve them in our application.

useMemo

You can use useMemo to save time and processing on the execution of a complex function.

Imagine that you have a function that receives some parameters and does count with them that's expensive to process. What the useMemo hook does is memorize these parameters and the returned value and whenever these parameters are equal again, it will return the previous value, without calculating it.

import { useMemo } from "react";

export default function Component() {
  const value = useMemo(() => {
    // Do something complex with x and y.
  }, [x, y]);
}

useCallback

Before explaining what the useCallback does, I'd like to explain an advanced and important concept in React.

React uses Strict Equality Comparison to compare the props or the observable values in a React Hook. If any of these values are different, it will update our component or call the method inside the React Hook.

And you should know like Patrick does that things aren't so obvious in javascript with non-primitive values.

alt

Primitive values are string, number and boolean. Array, object and function aren't primitives, so, if you compare them, you'll get false:

1 === 1; // true;
"batman" === "batman"; // true;
false === false; // true;

{} === {}; // false;
[] === []; // false;
() => {} === () => {}; //false;

It will only return true if it has the same reference, like a variable:

const fn = () => {};
fn === fn; // true;

So if you pass a function by props, React will update the component every time, even if the props haven't changed, because a function is different from a function.

Therefore we use useCallback to create a unique reference to a function and React knows that it's the same and avoids unnecessary updates.

Its reference only will be different in case the parameters change.

import { useCallback } from "react";

export default function Parent() {
  const onFetchAlwaysHandler = useCallback(() => {
    // Its reference will change when a and b change.
  }, [a, b]);

  const onFetchOnceHandler = useCallback(() => {
    // It will have the same reference.
  }, []);

  return <Approvad onFetchHandler={onFetchHandler} />;
}

Watch out

If you use a function with useCallback as a dependency of useEffect that updates the useCallback parameters, you can cause an infinite loop.

Improving our performance with React.memo

You can use useCallback and React.memo to improve the performance in your application and avoid that the children components update whenever that's an update on the parent component.

function List({ item, onSelect }) {
  return (
    <li>
      <button onClick={() => onSelect(item)}>Select {item}</button>
    </li>
  );
}

const MemorizedList = React.memo(List);

function App() {
  const onSelectHandler = useCallback((selectedItem) => {
    // Do something with selectedItem.
  }, []);

  return (
    <ul>
      {array.map((e) => (
        <MemorizedList item={e} onSelect={onSelectHandler} />
      ))}
    </ul>
  );
}

useLayoutEffect

The hook useLayoutEffect has the same functionality as useEffect, however, it will execute its function as soon the browser finishes mounting the dom.

You can use this hook to read an element in the DOM or do something when the page loads.

import { useState, useLayoutEffect } from "react";

function Component() {
  const [loadedDOM, setLoadedDOM] = useState(false);

  useLayoutEffect(() => {
    setLoadedDOM(true);
    window.localstorage.getItem("...");
  }, []);
}

SSR

If you're using Server Side Render (NextJS), the React Hooks useLayoutEffect and useEffect won't work because they only run on the client-side. If you try, React will show you a warning on the console.

You can use them to interact with the localstorage which's a feature only in the client, for example.

useDebugValue

The hook useDebugValue can be used to show a label on the React DevTools extension inside a custom hook. It works like a console.log.

import { useDebugValue, useState } from "react";

function useUserStatus() {
  const [isLogged, setIsLogged] = useState(false);

  //...

  useDebugValue(isLogged ? "Logged" : "Not logged");

  return isLogged;
}

useImperativeHandle

The useImperativeHandle hook allows you to customize the ref value in a parent component through its child component. You can pass a DOM element, method, anything and be able to call it from the parent component. It must be used with the React.forwardRef in the child component to receive the ref from the parent component.

import { useImperativeHandle, forwardRef, useEffect, useRef } from "react";

const Input = forwardRef((props, parentRef) => {
  const childRef = useRef();
  const someMethod = () => 1;

  useImperativeHandle(parentRef, () => ({
    focusOnChildInput: () => childRef.current.focus(),
    callChildMethod: someMethod,
  }));
  return <input ref={childRef} />;
});

const Form = () => {
  const ref = useRef();
  useEffect(() => {
    if (ref) {
      ref.current?.callChildMethod?.(); // 1
      ref.current?.focusOnChildInput?.();
    }
  }, []);

  return (
    <form>
      <Input ref={ref} />
    </form>
  );
};

Documentation

That's all, if you want to see more details or read about these React Hooks, please, take a look at the official React documentation.