Full-stack Web Technologies

CHAPTER 4
useEffect

Whenever we need to do things in components that are not strictly the rendering, like fetching data, or subscribing to a chat room, we need useEffect.

useEffect registers a function (the "effect") to be executed at certain times when the component re-renders, and declares what values will, if changed, trigger a new execution.

Every time

To execute an effect every time your component renders, just use the first parameter:

console.log(`Component was rendered!`);

useEffect(() => {
  console.log(`useEffect was called!`);
});

The first time

To execute an effect just the first time a component renders (to fetch data, for instance), the second parameter has to be an empty array. Here is a component showing a list of random user names from randomuser.me

"use client";

import { useEffect, useState } from "react";

type UserList = Array<Record<string, any>>;

const loadUsers = async () => {
  const response = await fetch(`https://randomuser.me/api?results=20`);
  const { results: users } = await response.json();
  return users;
};

export default function RandomUserList() {
  const [userList, setUserList] = useState<UserList>([]);

  useEffect(() => {
    loadUsers().then(setUserList);
  }, []);

  return (
    <div>
      {userList.map(({ name }) => (
        <div>
          {name.first} {name.last}
        </div>
      ))}
    </div>
  );
}

Notice two things:

  • We had to pass a type to useState to indicate what type is the value we are storing (otherwise, just with the [] it cannot be deduced).

  • We have to call loadUsers without await because useEffect functions cannot be asynchronous, so we use then to set a callback. Also the callback can be directly be setUserList, since it is expecting, as a first parameter, the data to be kept.

Depending on certain values

In general, a component has two types of data inputs: prop values and state values. Props change because a parent has sent different values from last time. State changes due to events, typically. But those inputs are the ones that are shown on the screen in some way. If any of those change, the component has to re-render.

This is still true if the input values are used in a fetch returning data that is ultimately shown on the screen, since changing the values will need to re-execute the fetch to make it "fresh".

Such is the case with search:

"use client";

import { useEffect, useState } from "react";

type BeerList = Array<Record<string, any>>;

const BASE_URL = `https://api.escuelajs.co/api/v1`;

const searchProducts = async (search: string) => {
  const response = await fetch(`${BASE_URL}/products?title=${search}`);
  const products = await response.json();
  console.log(products);
  return products;
};

export default function SearchBeers({ search }: { search: string }) {
  const [products, setProducts] = useState<BeerList>([]);

  useEffect(() => {
    searchProducts(search).then(setProducts);
  }, [search]);

  return (
    <div>
      {products.map((product) => (
        <p key={product.id}>{product.title}</p>
      ))}
    </div>
  );
}

This component receives a search prop, which tell it to show products filtered by that value. To get them, the component has to do a fetch every time the search prop changes, and therefore, the search value is in the array as the second parameter to useEffect.