top of page
Writer's pictureTandid Alam

React Hooks

Updated: Nov 5, 2022

Understanding the different hooks within the React library

Contents


- What is React Hooks?

- Basic Hooks

- useState

- useEffect

- useContext

- Additional Hooks

- useRef

- useReducer

- useCallback

- useMemo

- useLayoutEffect

- useImperativeHandle

- CustomHooks

- Learning Resources


 

What is React Hooks?


Hooks allow function components to have access to state and other React features. Because of this, class components are generally no longer needed.


There are 3 rules for hooks:

  • Hooks can only be called inside React function components.

  • Hooks can only be called at the top level of a component.

  • Hooks cannot be conditional


 

Basic Hooks

useState( )


The React useState Hook allows us to track state in a function component.


State generally refers to data or properties that need to be tracking in an application.


We initialize our state by calling useState in our function component.

useState accepts an initial state and returns two values:

  • The current state.

  • A function that updates the state.


import { useState } from "react";

function FavoriteColor() {
  const [color, setColor] = useState("");
}

//color is our current state
//setColor is a function that we use to update our state
//useState initializes our initial state, color, to an empty string


To update our color state, let's make use of setColor.


function Color() {
  const [color, setColor] = useState("red");

  return (
    <>
      <h1>{color}!</h1>
      <button onClick={() => setColor("blue")}> Change Color </button>
    </>
  )
}

//When we click the button, it changes the color from red to blue


Our initial state can also be an object.

function Car() {
  const [car, setCar] = useState({
    brand: "Ford",
    model: "Mustang",
    year: "1964",
    color: "red"
  });

  return (
    <>
      <h1>My {car.brand}</h1>
      <p>
        It is a {car.color} {car.model} from {car.year}.
      </p>
    </>
  )
}

When state is updated, the entire state gets overwritten. That said, make sure to use the spread operator to prevent this.

const updateColor = () => {
  setCar(previousState => {
    return { ...previousState, color: "blue" }
  });
}


useEffect( )


The useEffect Hook allows you to perform side effects in your components. Some examples of side effects are: fetching data, directly updating the DOM, and timers.


useEffect accepts two arguments. The second argument is optional.

useEffect(<function>, <dependency>)


Let's take a look at an example of a timer below:

import { useState, useEffect } from "react";

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

  useEffect(() => {
    setTimeout(() => {
      setCount((count) => count + 1);
    }, 1000);
  });

  return <h1>I've rendered {count} times!</h1>;
}

The problem with this is that it keeps counting because useEffect runs on every render. When count changes, a render happens, which then triggers the useEffect again endlessly. There's ways to fix this by using the second parameter.



1. No dependency
useEffect(() => {
  //Runs on every render
});


2. An empty array
useEffect(() => {
  //Runs only on the first render
}, []);


3. Props or state values
useEffect(() => {
  //Runs on the first render
  //And any time any dependency value changes
}, [prop, state]);

Clean Up: We can clean up useEffect with the return function at the end in order to get rid of any memory leaks.


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

  useEffect(() => {
    let timer = setTimeout(() => {
    setCount((count) => count + 1);
  }, 1000);

  return () => clearTimeout(timer)
  }, []);

  return <h1>I've rendered {count} times!</h1>;
}


useContext( )


React Context is a way to manage state globally. It can be used together with the useState Hook to share state between deeply nested components more easily than with useState alone. State should be held by the highest parent component in the stack that requires access to the state. Passing down props is known as prop drilling though and is very tedious.



First we need to create a context with createContext() and wrap child components in the Context Provider and supply the state value.

import { useState, createContext } from "react";
const UserContext = createContext()


function Component1() {
  const [user, setUser] = useState("Jesse Hall");

  return (
    <UserContext.Provider value={user}>
      <h1>{`Hello ${user}!`}</h1>
      <Component2 user={user} />
    </UserContext.Provider>
  );
}
//Now, all components in this tree will have access to the user Context.


Since we wrapped our child components with Context Provider, these components can access the state no matter how deeply nested they are. The below example shows the fifth component accessing the user state from the first component through useContext().

import { useState, createContext, useContext } from "react";

function Component5() {
  const user = useContext(UserContext);

  return (
    <>
      <h1>Component 5</h1>
      <h2>{`Hello ${user} again!`}</h2>
    </>
  );
}

 


Additional Hooks

useRef( )


The useRef Hook allows you to persist values between renders. It can be used to store a mutable value that does not cause a re-render when updated.


If we tried to count how many times our application renders using the useState Hook, we would be caught in an infinite loop since this Hook itself causes a re-render. To avoid this, we can use the useRef Hook.


Use useRef to track application renders.

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>
    </>
  );
}

useRef() only returns one item. It returns an Object called current. When we initialize useRef we set the initial value: useRef(0) => {count.current: 0}



The useRef Hook is really good for taking html elements but can also be used to keep track of previous state values. This is because we are able to persist useRef values between renders.


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

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>
    </>
  );
}


useReducer( )


The useReducer Hook is similar to the useState Hook.

It allows for custom state logic.

If you find yourself keeping track of multiple pieces of state that rely on complex logic, useReducer may be useful.


The useReducer Hook returns the current state and a dispatch method.


useReducer(<reducer>, <initialState>)



import { useReducer } from "react";

const initialTodos = [
  {
    id: 1,
    title: "Todo 1",
    complete: false,
  },
  {
    id: 2,
    title: "Todo 2",
    complete: false,
  },
];

const reducer = (state, action) => {
  switch (action.type) {
    case "COMPLETE":
      return state.map((todo) => {
        if (todo.id === action.id) {
          return { ...todo, complete: !todo.complete };
        } else {
          return todo;
        }
      });
    default:
      return state;
  }
};

function Todos() {
  const [todos, dispatch] = useReducer(reducer, initialTodos);

  const handleComplete = (todo) => {
    dispatch({ type: "COMPLETE", id: todo.id });
  };

  return (
    <>
      {todos.map((todo) => (
        <div key={todo.id}>
          <label>
            <input
              type="checkbox"
              checked={todo.complete}
              onChange={() => handleComplete(todo)}
            />
            {todo.title}
          </label>
        </div>
      ))}
    </>
  );
}


useCallback( )


The React useCallback Hook returns a memoized callback function. Think of memoization as caching a value so that it does not need to be recalculated.


This allows us to isolate resource intensive functions so that they will not automatically run on every render.

The useCallback Hook only runs when one of its dependencies update.

This can improve performance.



useMemo( )


The React useMemo Hook returns a memoized value.

Think of memoization as caching a value so that it does not need to be recalculated.


The useMemo Hook only runs when one of its dependencies update.

This can improve performance.


The useMemo Hook can be used to keep expensive, resource intensive functions from needlessly running.

In this example, we have an expensive function that runs on every render.

When changing the count or adding a todo, you will notice a delay in execution.


To fix this performance issue, we can use the useMemo Hook to memoize the expensiveCalculation function. This will cause the function to only run when needed.


We can wrap the expensive function call with useMemo.


The useMemoHook accepts a second parameter to declare dependencies. The expensive function will only run when its dependencies have changed.


In the following example, the expensive function will only run when count is changed and not when todo's are added


import { useState, useMemo } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);
  const calculation = useMemo(() => expensiveCalculation(count), [count]);

  const increment = () => {
    setCount((c) => c + 1);
  };
  const addTodo = () => {
    setTodos((t) => [...t, "New Todo"]);
  };

  return (
    <div>
      <div>
        <h2>My Todos</h2>
        {todos.map((todo, index) => {
          return <p key={index}>{todo}</p>;
        })}
        <button onClick={addTodo}>Add Todo</button>
      </div>
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
        <h2>Expensive Calculation</h2>
        {calculation}
      </div>
    </div>
  );
};

const expensiveCalculation = (num) => {
  console.log("Calculating...");
  for (let i = 0; i < 1000000000; i++) {
    num += 1;
  }
  return num;
};


customHooks( )


Hooks are reusable functions. When you have component logic that needs to be used by multiple components, we can extract that logic to a custom Hook. Custom Hooks start with "use". Example: useFetch.



//useFetch.js

import { useState, useEffect } from "react";

const useFetch = (url) => {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch(url)
      .then((res) => res.json())
      .then((data) => setData(data));
  }, [url]);

  return [data];
};

export default useFetch;



//index.js

import { useState, useEffect } from "react";

const useFetch = (url) => {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch(url)
      .then((res) => res.json())
      .then((data) => setData(data));
  }, [url]);

  return [data];
};

export default useFetch;



We have created a new file called useFetch.js containing a function called useFetch which contains all of the logic needed to fetch our data.


We removed the hard-coded URL and replaced it with a url variable that can be passed to the custom Hook.


Lastly, we are returning our data from our Hook.


In index.js, we are importing our useFetch Hook and utilizing it like any other Hook. This is where we pass in the URL to fetch data from.


Now we can reuse this custom Hook in any component to fetch data from any URL.



 

Learning Resources


Comments


bottom of page