reactjstypescriptasync-awaitreact-tsx

Using async to display form in React when API handling is in another class


When I started my project, I made a class that handles the API data. Now I can't figure out how to manage the async to properly display the data. I have a console.log in fetchOpenings that will show my list on console, but it won't display it to the screen. It will display my data if I edit the code.

export class ChessGameFetcher {

  async fetchGameArchives(): Promise<void> {
    try {
      const response = await fetch(this.apiURL);
      if (response.ok) {
        const data = (await response.json()) as ChessGames;
        this.games = data.games;
      } else {
        throw new Error("Network response was not ok");
      }
    } catch (error) {
      if (error instanceof Error) {
        console.error(
          "There has been a problem with your fetch operation: ",
          error.message
        );
      } else {
        console.error("An unexpected error occurred");
      }
    }
  }


  logOpenings(): void {
    this.games.forEach((game) => {
      let opening = game.eco;
      let accuracyWhite = game.accuracies?.white ?? 0;
      let accuracyBlack = game.accuracies?.black ?? 0;

      let date: Date = new Date(2024, 4);

      const extractedDate = this.extractUTCDateTime(game.pgn);

      if (extractedDate) {
        date = extractedDate;
      }

      this.processChessString(game.pgn);

      const sidePlayed =
        game.white.username === this.username ? "white" : "black";
      let result =
        sidePlayed === "white" ? game.white.result : game.black.result;

      result = this.normalizeResult(result);

      const match = opening.match(this.regex);
      if (match && match[1]) {
        opening = match[1].replace(/-$/, "").replace(/-/g, " ");
      }
      const matchedOpening = this.predefinedOpenings.find((openingName) =>
        opening.includes(openingName)
      );

      if (matchedOpening) {
        this.updateOpeningResults(matchedOpening, result, opening);
      } else {
        console.log(`Unmatched opening: ${opening}`);
      }

      this.gameInfo.push(
        new GameInfo(
          result,
          opening,
          sidePlayed,
          game.black.username,
          accuracyWhite,
          accuracyBlack,
          date
        )
      );
    });
  }


  async init(): Promise<void> {
    await this.fetchGameArchives();
    this.logOpenings();
    this.returnOpeningData();
    this.getPercentages();
    //console.log(this.user_opening_percentages);
  }
}
import React, { useState, useEffect } from "react";
import { ChessGameFetcher } from "./classes/ChessGameFetcher";
import { PercentageInfo } from "./classes/PercentageInfo";

function OpeningForm() {
  const [stat, setStat] = useState<PercentageInfo[]>([new PercentageInfo(0, 0, 0, 0), new PercentageInfo(0, 0, 0, 0)]);
  const [username, setUsername] = useState("");
  const [loading, setLoading] = useState(false);
  const [submitted, setSubmitted] = useState(false);
  const listStats = stat.map((st, index) => (
    <li key={index}>{st.toString()}</li>
  ));

  async function fetchOpenings() {
    const gameFetcher = new ChessGameFetcher(username);
    const openingStats: PercentageInfo[] = gameFetcher.user_opening_percentages;
    console.log(openingStats);
    setStat(openingStats);
  }


  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    setSubmitted(true); // Trigger the useEffect to fetch data
    fetchOpenings();
  };

  return (
    <>
      <form onSubmit={handleSubmit}>
        <label>
          Enter your name:
          <input
            type="text"
            value={username}
            onChange={(e) => setUsername(e.target.value)}
          />
        </label>
        <button type="submit">Submit</button>
      </form>

      <ul>{listStats}</ul>
    </>
  );
}

export default OpeningForm;

I've tried adding a useEffect which didn't seem to do anything. I've tried to call await on setSet(), but that doesn't really do anything.


Solution

  • Your use of an async constructor is a bit odd. I suggest refactoring your ChessGameFetcher class to something along these lines:

    export class ChessGameFetcher {
      username: string // Not 100% if you need this, but you use it when instantiating your class
    
      constructor(username: string) {
        this.username = username
      }
    
      async fetchGames() {
        await this.fetchGameArchives();
        this.logOpenings();
        this.returnOpeningData();
        this.getPercentages();
      }
    
      ... // everything else you already have except the init() method
    }
    

    The idea of the change is to separate your async functionality into its own method so you can explicitly wait for it to run before calling setStat. Now you can initialize your class in your React component like this:

    async function fetchOpenings() {
      const gameFetcher = new ChessGameFetcher(username);
      // By using await we ensure we don't try to setStat before all the games are fetched
      await gameFetcher.fetchGames();
      const openingStats: PercentageInfo[] = gameFetcher.user_opening_percentages;
      console.log(openingStats);
      setStat(openingStats);
    }
    

    Once you've set this up you can load the <ul> from your stat variable. Delete listStats, this variable in your code is only initialized once when your component is rendered and will never work.

    return (
      <>
        <form onSubmit={handleSubmit}>
          <label>
            Enter your name:
            <input
              type="text"
              value={username}
              onChange={(e) => setUsername(e.target.value)}
            />
          </label>
          <button type="submit">Submit</button>
        </form>
        <ul>{
          stat.map((st, index) => (<li key={index}>{st.toString()}</li>))
        }</ul>
      </>
    )