Welcome back to part 4 of this tutorial series! In this part, we’ll add in ChatGPT, but before we get there, it’s time to apply the knowledge we’ve gained and fetch our t-rex todos list from our own API endpoint.
Fetching data from our own API
So let’s go back to our page.tsx file inside the blockbuster_chat folder and rewrite it in a better way.
π app
π api
π blockbuster
π route.tsx
π blockbuster_chat
π page.tsx π οΈ We'll be working on this file
π counter
π page.tsx
π favicon.ico
π globals.css
π layout.tsx
π navbar.tsx
π page.tsx
Inside this file, just remove everything we have and start with an empty page, as we’re going to be changing pretty much everything, so it’s easier to just start from scratch. Let’s first add our imports and the TypeScript interface for the Todo object data type:
"use client";
import React, { useEffect, useState } from "react";
interface Todo {
userId: number;
id: number;
title: string;
completed: boolean;
}
We start with the "use client"; pragma to indicate that this file will be running on the client side, as we saw before that this is needed to use the useState and useEffect hooks. We then import the necessary React hooks and define the Todo interface that represents the structure of a to-do item, which is the same one we had before.
Now let’s create our functional component BlockBusterChat, using all the knowledge we’ve gained so far. Note that we’ll use a function fetchTodos which doesn’t exist yet, we’ll take care of that next. First add:
const BlockBusterChat = () => {
const [todoList, setTodoList] = useState<Todo[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchTodos(setTodoList, setLoading, setError);
}, []);
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error}</div>;
}
return (
<>
<h1 className="font-bold">Blockbuster Chat</h1>
<ul>
{todoList.map((todo) => (
<li key={todo.id}>
{todo.id}. {todo.title} -{" "}
{todo.completed ? "Completed" : "Not completed"}
</li>
))}
</ul>
</>
);
};
export default BlockBusterChat;
So we start our functional component BlockBusterChat by defining three state variables using the useState hook. For the first one, for example, we get the variable name todoList, and the function to update it setTodoList. We call useState([]) to initialize it as an empty array of Todo objects to get started with. The <Todo[]> in between is just the TypeScript syntax to let TypeScript know that this state variable will be an array [] of Todo objects.
Second is the loading state variable with the setLoading function to update it, initialized as true. We tell TypeScript that this variable will be a <boolean> type true or false. Finally, we have the error state variable with the setError function to update it, initialized as null. We tell TypeScript that this variable will be a <string | null> type, meaning it can be either a string or null, with the | pipe symbol representing “or”.
Next we use the useEffect hook. We could define all the functionality inside here, but as we’re a bit new to this all, I decided to create a separate function fetchTodos to make it a bit easier to read and understand. We’ll define this function next, but we can see that all that useEffect does here is call the fetchTodos function. It passes in all three setState functions so that this fetchTodos function will have the ability to update all three state variables.
We then have two conditional checks. If loading is true, we return a simple loading message. If error is not null, we return an error message with the error string. Finally, if everything is fine and loading has been set back to false, we return the actual content of our page, which is a simple list of todos, using the .map function we learned about before to loop over each todo item in the list of todos and display it.
That’s all good so far, but of course we need to define the fetchTodos function. Let’s do that next. I will put this function before the BlockBusterChat component, right after the Todo interface:
const fetchTodos = async (
setTodoList: React.Dispatch<React.SetStateAction<Todo[]>>,
setLoading: React.Dispatch<React.SetStateAction<boolean>>,
setError: React.Dispatch<React.SetStateAction<string | null>>
) => {
try {
const res = await fetch("/api/blockbuster");
if (!res.ok) {
throw new Error("Network response was not ok");
}
const data: Todo[] = await res.json();
setTodoList(data);
} catch (error: unknown) {
if (error instanceof Error) {
setError(error.message);
} else {
console.log(error);
setError("An unknown error occurred");
}
} finally {
setLoading(false);
}
};
So we define an arrow function () => {} called fetchTodos. It has to be async as we know we’ll have to wait for some data fetching. We know that we take three state variable setter functions as input arguments (not the state variables themselves), so we define those first.
The React.Dispatch<React.SetStateAction<Todo[]>> part might look a bit complex, but just think of it dispatching a setstateaction that will update either a Todo[] array, boolean, or string | null state variable. This is just TypeScript syntax to define the type of the input arguments, and it is not needed to deeply understand it at this point.
Next we use a try, catch, finally block to handle the fetching of the todos. If you’ve worked with Python before, this is basically the same as the try, except, finally block. It will try to execute the code inside the try block, and if an error occurs, it will jump to the catch block. The finally block will always be executed, no matter if an error occurred or not, so is a great place to put code that should always be executed regardless.
Inside the try block, we fetch the todos from our API endpoint /api/blockbuster, note that we only have to provide the relative path to our API endpoint, which is really cool! We have to await the fetch call just like we did with previous API calls before, and catch the response in a constant named res.
We then check if the res response was not (! = not) ok, by accessing the responses’ ok property. If it was not ok, we throw an error with the message “Network response was not ok”.
Assuming there was no problem the code will move on to the next line, where we parse the response data as JSON and store it in a constant named data. This is exactly like we did before. This time we added another line though to update the todoList state variable with the fetched data using the setTodoList function.
Inside the catch block, it takes an input of error which is of type unknown. We then check if this error is an instance of the Error class, which is a built-in JavaScript class for errors. If it is, we set the error state variable to the error message of this error, using the setError state setter function.
If it’s not an instance of the Error class, we log the error to the console and set the error state variable to a generic error message. We’re not going to worry too much about perfecting error handling here.
Finally, inside the finally block, pun intended, we set the loading state variable to false, as we’re done loading the todos, regardless of if there was an error or not, so that the page can go on to display the results. Your full page.tsx file should now look like this:
"use client";
import React, { useEffect, useState } from "react";
interface Todo {
userId: number;
id: number;
title: string;
completed: boolean;
}
const fetchTodos = async (
setTodoList: React.Dispatch<React.SetStateAction<Todo[]>>,
setLoading: React.Dispatch<React.SetStateAction<boolean>>,
setError: React.Dispatch<React.SetStateAction<string | null>>
) => {
try {
const res = await fetch("/api/blockbuster");
if (!res.ok) {
throw new Error("Network response was not ok");
}
const data: Todo[] = await res.json();
setTodoList(data);
} catch (error: unknown) {
if (error instanceof Error) {
setError(error.message);
} else {
console.log(error);
setError("An unknown error occurred");
}
} finally {
setLoading(false);
}
};
const BlockBusterChat = () => {
const [todoList, setTodoList] = useState<Todo[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchTodos(setTodoList, setLoading, setError);
}, []);
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error}</div>;
}
return (
<>
<h1 className="font-bold">Blockbuster Chat</h1>
<ul>
{todoList.map((todo) => (
<li key={todo.id}>
{todo.id}. {todo.title} -{" "}
{todo.completed ? "Completed" : "Not completed"}
</li>
))}
</ul>
</>
);
};
export default BlockBusterChat;
Now if you load your page in the brow
