Skip to main content

useEffect & async code

We already verified our own server using Postman, now we will try to send a GET request from within our front-end code. For this to occur, we will have to touch upon two new subjects that we haven't touched upon so far: async code and React's useEffect hook.

Async code

We refer to code being async when a certain line of code holds a "promise" - which, simplified, means that the line of code will take a while (loading data, querying data, perform a timely function etc.) before its result can be used. Promises have three states:

  • Pending: The initial state, indicating that the asynchronous operation is still ongoing and the promise hasn't been fulfilled or rejected yet.
  • Fulfilled: The state when the asynchronous operation successfully completes, resulting in a value.
  • Rejected: The state when the asynchronous operation encounters an error or fails, resulting in a reason for the failure.

When dealing with asynchronous operations, we use async/await syntax, so that JavaScript knows when to pause and 'await' a certain function or action.

Let's start by writing an async-function that uses a GET request. It could look something like this:

async function getTimeEntries():Promise<Types.TimeEntry[]> {
const response = await fetch('http://localhost:3004/time-entries', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
});

return response.json();
}

In this example, we can immediately see that this function is explicitly defined as 'async' by writing this word in front of the function definition. When you prefix a function with async, you signal to JavaScript that the function body will contain lines that it will have to await, but only if we explicitly tell it to wait. In the example above you see that we await the result of a fetch instance. If you hover across fetch in VSC, you'll see an info pop-up that tells you a fetch returns a promise, which means the const that deploys the fetch function will take a while to return its response/become defined, and therefore we will have to "await" the fetch by prefixing the fetch with await. The return type for this service is similar to how you would create interfaces for components.

useEffect

useEffect is a React hook. It can be simplified as a function that runs whenever a component is mounted, and optionally whenever a constant changes value. It is used to run code whenever: a 'side-effect' occurs, such as a component prop that changes or a state that changes, or whenever the component mounts and it is first required to fetch data from an external source, or calculate a value before we can render that data in the component template. You add the variables that can trigger the useEffect to the dependency array. If you only want the function in your useEffect to run when the component gets mounted, use an empty dependency array.

The syntax for using a useEffect is the following:

import { useEffect } from 'react'

// within your component:
useEffect(() => {
// whatever you want to perform on component mount
}, [*optional: variables that trigger this useEffect whenever the variables change*])

Also make sure you're defining your useEffect in a client component.

useEffect & fetching data

Next, we will use a useEffect within our React component to call the "async function" we defined at the top to fetch the time entries data. Once the server has delivered our time entries data, we store the data in a component state using React's useState. Here's an example:

TimeEntries.tsx
"use client"

import { useEffect } from 'react'

const TimeEntries = () => {
const [timeEntries, setTimeEntries] = React.useState([]);

async function getTimeEntries():Promise<Types.TimeEntry[]> {
const response = await fetch('http://localhost:3004/time-entries', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
});

return response.json();
}

async function fetchTimeEntries() {
setTimeEntries(await getTimeEntries())
}

useEffect(() => {
fetchTimeEntries()
}, [])

return (
<>
<pre>{JSON.stringify(timeEntries)}</pre>
</>
)

}

Above, we see the useEffect being imported, and used within the React function body. It is only run when the component mounts (because the [] (dependency array) doesn't contain any other variables that trigger this useEffect), and when it does, it calls the fetchTimeEntries function. This function is defined in the React function body and is defined as async, as it awaits the returned data of getTimeEntries() and then puts these timeEntries in our useState, so that we may use this data to show our time entry data. getTimeEntries is also defined in the React function body as an async function, since it awaits the result of the fetch to our time entries JSON-server and returns this data.

useEffects are used to generate side-effects in your component. Fetch asynchronous data is a good example of this. It might seem complicated at first, so take your time to grasp why the functions are async, and why we use useEffect.

Debugging

Soon enough you'll encounter an issue that requires debugging, but how can this be done? Fortunately, every major browser comes with useful developer tools. We've used Chrome's developer tools before for inspecting and modifying HTML elements and React components. Now we'll take a close look at the "Network" tab.

You'll see an overview of all network requests the current page has made, which includes external JavaScript, stylesheets, images and more. When debugging REST calls we won't be needing all this, so set the filter to "Fetch/XHR".

In the initial overview the status code is already being shown, so any issues will stand out as there will be a 4xx or 5xx status code that you wouldn't expect. By clicking on one of the individual network calls you can have a closer look at them. Not only can you preview the response, you can also take a look at both the request and response headers and the applicable cookies. Altogether this will help you in figuring out what caused your issue.

When not to use useEffect

The useEffect hook might seem handy for a lot of situations, but be careful that you don't overuse it. Removing unnecessary effects will make your code easier to follow, faster to run, and less error-prone. If you can calculate something during render, you don't need an effect. For example, you might have a form component where you want to combine firstName and lastName to get fullName:

function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');

// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
}

This will cause unnecessary re-renders. When something can be calculated from the existing props or state, don't put it in a state, simply calculate it during rendering:

function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ Good: calculated during rendering
const fullName = firstName + ' ' + lastName;
}

Or maybe you want to reset a state when a prop changes. For example:

export default function ProfilePage({ userId }) {
const [comment, setComment] = useState('');

// 🔴 Avoid: Resetting state on prop change in an Effect
useEffect(() => {
setComment('');
}, [userId]);
}

A much easier and more efficient way to reset states is by using the prop as a key:

<Profile
userId={userId}
key={userId}
/>

Whenever the key changes, React will recreate the DOM and reset the state of the Profile component and all of its children.

For many more examples and some fun challenges, see You Might Not Need an Effect.

Further reading