I am new to web development and am currently building a web app that utilizes the IGDB API (https://www.igdb.com). Essentially, it’s a website where users can listen to game soundtracks and guess which game they belong to.
To select a game, I have an input field for the user to type the game name. This input should dynamically trigger the corresponding API call to retrieve the game data. I have tried using the API with hardcoded searches, and it works. However, when I create a ‘use client’ component to monitor the input state and make API calls based on that, it doesn’t work, and I receive this error:
This is the code of the page:
The page that uses “use client”:
"use client";
import { useState } from "react"; // Import useState hook from React for state management
import { TunePlayer } from "@/components/ui/audio-player/tune-player"; // Import TunePlayer component for playing tunes
import Search from "@/components/ui/game-ui/search"; // Import Search component for searching games
import { Input } from "@/components/ui/input"; // Import Input component for receiving user input
export default function Tune() {
// State to hold the search term
const [searchGame, setSearchGame] = useState({
name: "Kingdom Hearts", // Initial state with a default search term
});
// Function to update the search term based on user input
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchGame({ name: event.target.value }); // Update state with new search term
};
return (
<>
{/* Audio player component to play game tunes */}
<TunePlayer />
<div className="flex place-content-center place-items-center lg:flex-row gap-8 sm:gap-6 m-auto max-w-80">
{/* Search input field for entering game search terms */}
<Input
type="search"
placeholder="search your game"
onChange={handleSearchChange} // Trigger handleSearchChange on input change
/>
</div>
<div className="outline-8 outline-white justify-items-center grid gap-3 p-6">
{/* Displaying the current search term */}
<p>Searching for {searchGame.name}</p>
{/* Search component with search parameters passed as props */}
<Search searchParams={searchGame} />
</div>
</>
);
}
The search component:
import Link from "next/link";
import CustomImage from "@/components/ui/game-ui/custom-image";
import api from "@/api/igdb";
export default async function Search({ searchParams }: any) {
// Use the search function from the API with the given search parameters
const games = await api.search(searchParams);
console.log(games);
return (
<div className="px-4 xl:px-40">
{/* Check if any games were found */}
{games.length ? (
<div className="mt-4 grid grid-cols-3 sm:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-y-6 gap-x-2">
{/* Map over the array of games and display each game */}
{games.map((game: any) => (
// Use the Link component for navigation. Key is required for list items in React for unique identification.
<Link key={game.id} href={`/games/${game.id}`}>
{/* Figure tag used for semantic markup of image (CustomImage) and caption */}
<figure>
<div className="relative aspect-[3/4] rounded-xl c-bg-dark-blue transition md:hover:brightness-110">
{/* CustomImage component displays the game's image. Props are spread from the game object. */}
<CustomImage {...game} />
</div>
{/* Figcaption displays the game's name. Text size and margin adjustments for responsive design. */}
<figcaption className="mt-2.5 text-xs sm:mt-3 sm:text-base font-bold text-center line-clamp-3">
{game.name}
</figcaption>
</figure>
</Link>
))}
</div>
) : (
// Display a message if no games were found
<h2 className="mt-4">No games found</h2>
)}
</div>
);
}
And the api calls it is using:
import { API_URL, CLIENT_ID, TOKEN_URL } from "@/lib/igdb-config";
interface Token {
access_token: string; // Token to authenticate API requests.
}
interface RequestParams {
resource: string; // The endpoint to request data from.
body: string; // The body of the request, usually containing the query.
}
interface SearchParams {
name: string; // The name to search for.
[key: string]: any; // Allows for additional, dynamic parameters.
}
// Query to fetch games by their rating, with various filters applied.
const gamesByRating = `
fields
name,
cover.image_id;
sort aggregated_rating desc;
where aggregated_rating_count > 20 & aggregated_rating != null & rating != null & category = 0;
limit 12;
`;
// Query to fetch detailed information about a single game.
const fullGameInfo = `
fields
name,
summary,
aggregated_rating,
cover.image_id,
genres.name,
screenshots.image_id,
release_dates.platform.name,
release_dates.human,
involved_companies.developer,
involved_companies.publisher,
involved_companies.company.name,
game_modes.name,
game_engines.name,
player_perspectives.name,
themes.name,
external_games.category,
external_games.name,
external_games.url,
similar_games.name,
similar_games.cover.image_id,
websites.url,
websites.category,
websites.trusted
;`;
export const api = {
token: null as Token | null, // Initially, there's no token.
// Function to retrieve a new token from the TOKEN_URL.
async getToken(): Promise<void> {
try {
const res = await fetch(TOKEN_URL, { method: "POST", cache: "no-store" });
this.token = (await res.json()) as Token; // Parse and store the token.
} catch (error) {
console.error(error); // Log any errors that occur during the fetch operation.
}
},
// Function to make an authenticated request to the API.
async request({ resource, ...options }: RequestParams): Promise<any> {
if (!this.token) {
throw new Error("Token is not initialized."); // Ensure the token is present.
}
const requestHeaders: HeadersInit = new Headers();
requestHeaders.set("Accept", "application/json");
requestHeaders.set("Client-ID", CLIENT_ID);
requestHeaders.set("Authorization", `Bearer ${this.token.access_token}`);
return fetch(`${API_URL}${resource}`, {
method: "POST",
headers: requestHeaders,
...options,
})
.then(async (response) => {
const data = await response.json();
return data; // Return the parsed JSON data.
})
.catch((error) => {
return error; // Return any errors that occur during the fetch operation.
});
},
// Function to fetch games by their rating using a predefined query.
getGamesByRating(): Promise<any> {
return this.request({
resource: "/games",
body: gamesByRating, // Use the gamesByRating query.
});
},
// Function to fetch detailed information about a single game by its ID.
getGameById(gameId: number): Promise<any> {
return this.request({
resource: "/games",
body: `${fullGameInfo} where id = (${gameId});`, // Use the fullGameInfo query with a specific game ID.
});
},
// Function to search for games based on a name and optional additional fields.
search({ name = "", ...fields }: SearchParams): Promise<any> {
let str = ""; // Initialize an empty string for additional search parameters.
for (const [key, value] of Object.entries(fields)) {
str += ` & ${key} = ${value}`; // Append each additional field to the search query.
}
return this.request({
resource: "/games",
body: `
fields
name,
cover.image_id;
where
cover.image_id != null
${str};
${name ? `search "${name}";` : ""}
limit 50;`, // Construct the final search query.
});
},
};
await api.getToken(); // Initialize the token before exporting the API.
export default api;
When removing the runtime error does not occur. Also when I remove the use client and give it some searchParams manually it does work and the api gets called correctly.