我是 Web 开发新手,目前正在构建一个利用 IGDB API (https://www.igdb.com) 的 Web 应用程序。本质上,这是一个用户可以听游戏配乐并猜测自己属于哪个游戏的网站。
为了选择游戏,我有一个输入字段供用户输入游戏名称。此输入应动态触发相应的 API 调用以检索游戏数据。我尝试过使用 API 进行硬编码搜索,并且它有效。但是,当我创建“使用客户端”组件来监视输入状态并据此进行 API 调用时,它不起作用,并且收到此错误:
这是页面代码:
使用“使用客户端”的页面:
"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>
</>
);
}
搜索组件:
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>
);
}
API 调用它正在使用:
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;
删除时不会出现运行时错误。另外,当我删除 use 客户端并手动给它一些 searchParams 时,它确实可以工作并且 api 被正确调用。
首先,您需要知道您不能像对
async
组件那样标记客户端组件(使用“use client”指令)Search
。在 use client
组件上使用 Tune
指令会自动使您的 Search
组件成为客户端组件。根据我的最佳猜测,错误应该是 You cannot make client components async
行中的内容。无论如何,你明白了。如果您删除 use client
指令,它将起作用,因为现在您的 Tune
和 Search
组件都是服务器组件,您可以为其创建组件 async
并且由于您已经提供了搜索名称,因此 api 调用是成功了,一切都很好。另请注意,服务器组件仅在请求时间或构建时间期间在服务器上呈现一次,具体取决于页面是动态还是静态。现在你有两个选择来继续你的逻辑。
保持
Search
组件不变(异步)并使用 page
组件的 searchParams属性。现在,每当用户在输入字段中键入内容时,您都会更新查询字符串(searchParams)而不是使用状态,这将导致服务器请求,并且您的页面将使用更新后的 searchParams 对象呈现,您可以将其传递给您的
Tune
组件然后到您的 Search
组件。
您保持
Tune
组件不变,并修改 Search
组件以使用客户端数据获取,即在 useEffect
内获取数据或使用其他客户端数据获取库(如 useSWR
或 react-query
)
额外:
children
属性。您接受服务器组件作为客户端组件内的 children
道具并渲染它。如果您有任何后续问题,请发表评论。