DEV Community

Megan Moulos
Megan Moulos

Posted on

React Hooks

Hooks were introduced to React 16.8. Hooks allow you to use things like state without writing a class-based component. In essence, Hooks are JavaScript functions that allow you to "hook into" React state and lifecycle features from function components. There are a number of built-in Hooks as well as the ability to write your own custom Hooks.

Note: It's important to remember that Hooks are backwards compatible, so you can use them with class components without a problem.

All React Hook names are prefixed with the word "use," like useState or useEffect. To use Hooks, you must import them first. You can enter any number of Hooks at the top of the file like so:

import React, { useState, useEffect } from "react";
Enter fullscreen mode Exit fullscreen mode

Why use hooks?

When compared to class components, hooks reduces the amount of code we need to write (and read). Take a look at this example:

With Hooks we can organize logic inside of a component. These units can be reusable, helping us avoid duplicated logic. Dan Abramov writes that "Hooks apply the React philosophy (explicit data flow and composition) inside a component, rather than just between the components." Hooks are fully encapsulated, and are a way to share stateful logic.

Here are some reasons to use Hooks from Shedrack Akintayo

  • Improved code reuse
  • Better code composition
  • Better defaults
  • Sharing non-visual logic with the use of custom Hooks
  • Flexibility in moving up and down the components tree

Rules

There are two important rules for using Hooks:

  • Only call Hooks at the top level. Don't call Hooks inside of loops, conditions, or nested functions.

  • Only call Hooks from React functions. Do not call Hooks from regular JavaScript functions. You can and should call Hooks from React function components. You may also call Hooks from other custom Hooks.

There is a plug-in called esLint which will enforce the rules of Hooks. Find and install it here!

You can use multiple Hooks in a single component, and React will rely on the order in which Hooks are called. Dan Abramov also states:

The ability to pass data between Hooks make them a great fit for expressing animations, data subscriptions, form management, and other stateful abstractions. Unlike render props or higher-order components, Hooks don’t create a “false hierarchy” in your render tree. They’re more like a flat list of “memory cells” attached to a component. No extra layers.


Built-in Hooks

React has 10 built-in Hooks, here are some of the more commonly used ones:


useState

The useState Hook is called inside of a function component to add local state to it. It allows you to create, update, and manipulate state inside the functional component it is used in. useState returns a destructured array containing two values: the first is the current state; the second is a setter function used to update the state. useState takes a single argument, which is it's initial state.

const [state, setState] = useState(initialState);
Enter fullscreen mode Exit fullscreen mode

Here is an example of a simple counter:

import React, { useState } from "react";

function Counter(){
    // The new state variable is called "count" here, and the initial value will be 0 since the counter will begin at 0.
    const [count, setCount] = useState(0);

    return (
       <div>
          <p>The button was clicked {count} times!</p>
          <button onClick={() => setCount(count + 1)}>
               Click to Count
          </button>
       </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

The new state variable is called count, and the setter function is called setCount. You can call these two variables anything you'd like, but this is the typical naming convention: use "set" concatenated with the variable name. The initial state is set to 0 since we want the counter to begin at 0. The setCount function will be called each time the button is clicked, and update the count variable by 1. React will remember the current value of count between re-renders, and provide the most recent one to the function component. If we want to update it, we just call setCount again.

As you can see, setCount was called inside of the function's return, namely in the JSX code of the button's onClick. This is perfectly fine!

You can use the state
Hook multiple times in a single function component:

function ManyStatesExample() {
    const [name, setName] = useState("Jimmy");
    const [age, setAge] = useState(35);
    const [favoriteColors, setFavoriteColors] = useState(["red", "blue"]);
Enter fullscreen mode Exit fullscreen mode

Note: Updating a state variable replaces it's contents.


useEffect

The useEffect Hook replicates React's lifecycle methods, but inside of a functional component. The official React documentation states: "If you’re familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount, componentDidUpdate, and componentWillUnmount combined." The useEffect Hook lets you perform side effects (such as external API interactions and data fetching). useEffect accepts two arguments:

  • A function with the code to run
  • A dependency array, which tells the Hook how many times to run. If this is left out, the Hook will run after every render. Be careful, leaving out the dependency array can cause infinite loops!

If you only want to run the useEffect Hook when certain values in your function have changed, you must pass the variables as dependencies into the array. If you supply an empty array, the Hook will only run once. This can be very useful for things such as fetch requests:

useEffect(() => {
    fetch("http://somelink.com");
}, []);
Enter fullscreen mode Exit fullscreen mode

The dependency array is empty because we only need to GET our data once.

Sometimes the useEffect Hook can require cleanup, which is written about in some detail in the React docs.


useContext

The useContext Hook allows you to make particular data accessible to all components throughout the whole of the application (consider it "global" in scope for a tree of React components). It works with the React Context API. This Hook is powerful because data can be passed across all components in an app, rather than having to manually pass a prop down through various levels to get data to a child.

For example, let's say you have an array of objects, and would like to pass that information down to a child function.

const menuItems = [
{ 
   dish: "spaghetti and meatballs",
   price: "$12.99",
},
{  
   dish: "steak and potatoes",
   price: "$29.99",
},
];

export const MenuContext = React.createContext(menuItems);

Enter fullscreen mode Exit fullscreen mode

We are creating a context object using React.createContext and now can pass menuItems to any components in the app. Each context needs to be enclosed in a Provider wrapper, like so:

function Menu() {
    return (
    <MenuContext.Provider value={menuItems}> 
        <DinnerMenu />
    </MenuContext.Provider>
);
}
Enter fullscreen mode Exit fullscreen mode

Now DinnerMenu becomes the consuming component of the context, and can use the objects stored in the MenuItems array. When the Provider updates, the Hook will trigger a rerender with the latest context value. See React's advanced Context guide here.


useReducer

useReducer can be thought of as an alternative to the useState Hook. The useReducer Hook can work with more complex logic, and lets you rerender the UI whenever values change within the Hook. It accepts two arguments: a reducer function and an initial state.

useReducer(reducerFunction, initialState);

Much like useState, useReducer returns an array of two values, which can be destructured to: the current value of the state; and a dispatch function.

const [state, dispatchFunction] = useReducer(reducerFunction, initialState);
Enter fullscreen mode Exit fullscreen mode

state is the current value of the initialState passed to the Hook. The reducerFunction accepts the state and an action. These two arguments will determine how the value of the state will change. The dispatchFunction is how an action is passed to the reducerFunction. There is usually a switch statement with a number of cases to determine how the value of state will change. Here is an example of a simple counter from the official documentation:

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Note: React guarantees that dispatch function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.

useRef

The useRef Hook allows you to persist values between renders. You can store a mutable value that does not cause a re-render when updated. useRef returns an Object called current. Consider useRef like a "box" that can hole a mutable value in this current property.

You can use this Hook to keep track of previous state values, because useRef persists it's values between renders. Consider the following example from W3Schools:

import { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom/client";

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

  useEffect(() => {
    previousInputValue.current = inputValue;
  }, [inputValue]);

  return (
    <>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <h2>Current Value: {inputValue}</h2>
      <h2>Previous Value: {previousInputValue.current}</h2>
    </>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
Enter fullscreen mode Exit fullscreen mode

See this blog post by Dmitri Pavlutin for more information and use cases of useRef.


Other Built-in Hooks

  • useCallback
  • useMemo
  • useImperativeHandle
  • useLayoutEffect
  • useDebugValue

Build Your Own Hooks

Along with these Hooks you can write your own custom Hooks that let you extract component logic into reusable, sharable functions. Because both components and Hooks are functions, you can extract the logic to a third, shared function (a new Hook!). A custom Hook is a JavaScript function whose name starts with "use" and that may call other Hooks. It still follows the rules of built-in Hooks, but you get to decide which arguments it will take and what it should return.

For more information on building custom Hooks, see: Building Your Own Hooks.


Further Reading:

Latest comments (0)