使用动态 API 调用组件时遇到错误(未处理的运行时错误):如何解决? (NextJS)

问题描述 投票:0回答:1

我是 Web 开发新手,目前正在构建一个利用 IGDB API (https://www.igdb.com) 的 Web 应用程序。本质上,这是一个用户可以听游戏配乐并猜测自己属于哪个游戏的网站。

为了选择游戏,我有一个输入字段供用户输入游戏名称。此输入应动态触发相应的 API 调用以检索游戏数据。我尝试过使用 API 进行硬编码搜索,并且它有效。但是,当我创建“使用客户端”组件来监视输入状态并据此进行 API 调用时,它不起作用,并且收到此错误:

Error

这是页面代码:

使用“使用客户端”的页面:

"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 被正确调用。

typescript next.js react-hooks
1个回答
0
投票

首先,您需要知道您不能像对

async
组件那样标记客户端组件(使用“use client”指令)
Search
。在
use client
组件上使用
Tune
指令会自动使您的
Search
组件成为客户端组件。根据我的最佳猜测,错误应该是
You cannot make client components async
行中的内容。无论如何,你明白了。如果您删除
use client
指令,它将起作用,因为现在您的
Tune
Search
组件都是服务器组件,您可以为其创建组件
async
并且由于您已经提供了搜索名称,因此 api 调用是成功了,一切都很好。另请注意,服务器组件仅在请求时间或构建时间期间在服务器上呈现一次,具体取决于页面是动态还是静态。现在你有两个选择来继续你的逻辑。

  1. 保持

    Search
    组件不变(异步)并使用 page 组件的
    searchParams
    属性。现在,每当用户在输入字段中键入内容时,您都会更新查询字符串(searchParams)而不是使用状态,这将导致服务器请求,并且您的页面将使用更新后的 searchParams 对象呈现,您可以将其传递给您的
    Tune
    组件然后到您的
    Search
    组件。

  2. 您保持

    Tune
    组件不变,并修改
    Search
    组件以使用客户端数据获取,即在
    useEffect
    内获取数据或使用其他客户端数据获取库(如
    useSWR
    react-query

额外:

  • 当前在客户端组件中使用服务器组件的唯一方法是通过
    children
    属性。您接受服务器组件作为客户端组件内的
    children
    道具并渲染它。
  • 您可以通过直接导入来在服务器组件中使用客户端组件。
  • 目前在 NextJs 应用程序路由器中处理搜索参数更新时非常痛苦,因此请查看这个库 nuqs,它也支持浅层更新。

如果您有任何后续问题,请发表评论。

© www.soinside.com 2019 - 2024. All rights reserved.