简体   繁体   中英

Need help, trying to convert a project from React to React with TypeScript

I am to convert this React project from no TypeScript to TypeScript and I am not sure what I am doing wrong here.

Here is the code for the component I am working on:

import React from "react";
import { useContext, useState, useEffect } from "react";
import styles from "./Search.module.css";
import { BsSearch } from "react-icons/bs";
import axios from "axios";
import { AuthContext } from "../../context/AuthContext";
import { useForm, SubmitHandler } from "react-hook-form";
import { Alert } from "react-bootstrap";
import SearchResult from "./SearchResult/SearchResult";

const Search: React.FC = () => {

  interface searchData {
    query: string | null;
    search: Function
  }

  interface localStorageItems {
    searchTerm: string | null;
    data: Array<any>;
  }

  const {
    register,
    handleSubmit,
    getValues,
    setValue,
    formState: { errors },
  } = useForm<searchData>({
    mode: "onSubmit",
    reValidateMode: "onChange",
    defaultValues: {},
    resolver: undefined,
    context: undefined,
    criteriaMode: "firstError",
    shouldFocusError: true,
    shouldUnregister: false,
  });

  const { state } = useContext(AuthContext);

  const [results, setResults] = useState<Array<any> | null>([]);

  const [isLoading, setIsLoading] = useState<Boolean>(false);

  useEffect(() => {
    const storedResults: localStorageItems = JSON.parse(
      window.localStorage.getItem("results") as string
    );
    if (storedResults) {
      setValue("query" , storedResults.searchTerm );
      setResults(storedResults.data);
    }
  }, [setValue]);

  const [apiError, setApiError] = useState<String>();

  const search: Function = async (data: any) => {
    try {
      setIsLoading(true);
      const asyncResponse = await axios({
        method: "GET",
        url: `/api/book/search/${data.query.split(" ").join("+")}`,
        headers: {
          Authorization: `Bearer ${state.token}`,
          "Content-Type": "application/json",
        },
      });
      if (asyncResponse.status === 200) {
        setIsLoading(false);
        setResults(asyncResponse.data.books);
        window.localStorage.setItem(
          "results",
          JSON.stringify({
            searchTerm: getValues("query"),
            data: asyncResponse.data.books,
          })
        );
      }
    } catch (e) {
      console.log(e);
      if (e.response) {
        setIsLoading(false);
        setApiError(
          "There was a problem with your search, please try again later or contact customer support."
        );
      }
    }
  };

  const searchQuery: string | null = getValues("query");

  const onSubmit: SubmitHandler<searchData> = (data) => search(data);

  return (
    <>
      {apiError && (
        <Alert className="mt-1" variant="danger">
          {apiError}
        </Alert>
      )}
      {isLoading && (
        <Alert className="mt-1" variant="secondary">
          Loading...
        </Alert>
      )}
      <h1 className="text-center">Search</h1>
      <form
        onSubmit={handleSubmit(onSubmit)}
        id={styles["search-form"]}
        className="d-flex col-md-4 my-2"
      >
        <input
          className="form-control me-2"
          type="search"
          placeholder="Enter your search here"
          aria-label="Search"
          onClick={() => {
            setValue("query", null);
            setResults(null);
            window.localStorage.removeItem("results");
          }}
          {...register("query", {
            required: "Please enter a search query",
          })}
        />
        {errors.query && (
          <Alert className="mt-1" variant="danger">
            {errors.query?.type === "required" && errors.query.message}
          </Alert>
        )}
        <button
          id={styles["search-button"]}
          className="btn btn-outline-success"
        >
          {<BsSearch />}
        </button>
      </form>
      {window.localStorage.getItem("results") && (
        <h2 className="text-center">
          Search results for "{searchQuery}"
        </h2>
      )}
      <div id={styles["searchResults"]}>
        {results &&
          results.map((result, index) => {
            return (
              <SearchResult
                key={`searchResult-${index}`}
                title={result.title}
                author={result.authors}
                desc={result.description}
                picture={result.imageLinks?.thumbnail}
                bookId={result.id}
              />
            );
          })}
      </div>
    </>
  );
};

export default Search;

I am using the useForm third party React Hook in this component and the issue seems to be to calls from functions in the hook itself. Now the error I am getting is when I call "getValues()" on line 91. It gives me the following error:

Type of property 'caller' circularly references itself in mapped type '{ [K in keyof Function]-?: PathImpl<K & string, Function[K]>; }'.

I am new to TypeScript and I am not entirely sure I understand what this means, at all. I have also done some googling but I cannot really find a solution to the problem, at least one that I understand.

Any help would be greatly appreciated!

If you did not manage to solve this, try this. Your interface searchData has a Function type.

Let me simplify the issue and make you think of it this way - you are trying to use useForm with a type that has a non serializable type inside it ie Function. A form typically only takes in serializable data types as input. Intuitively thinking about it, a function cannot be an input type for form, right? Even in your code, you are using 'setValue' with 'query' only.

So for useForm your type parameter should omit the search function. something like useForm<Omit<searchData, "search">> or have a separate type for the Omit and pass it to useForm if you want to reuse it eg in a context consumer.

That should sort you out. But as I said, I oversimplified the issue so that you can understand how to fix your code. Now, if you are interested for more info, the crux of the issue is that react hook form's watch function takes a type parameter and recursively looks for the keys in the provided type basically so it can go deep into nested form values (One of things i loved about react hook form). The problem you have with 'Function' as one of those types is that a Function type is recursive by design. If you look at the Function type in javascript, it has a property 'caller' whose type is Function. This essentially means, typescript would get into an endless loop if it was to continue checking the recursive generic provided by react hook form and so what does it do? You got it right, throws an error to avoid that mess!

One could argue that the issue lies with TypeScript but I think by design TS is okay with that decision. React hook Form should probably consider modifying the PathImpl generic to only accept type parameters with no recursive types such as Object and Function.

I had a similar scenario where one of my types had the javascript Object type as one of the property types. Was a bit tricky to notice!

PS: If you were to change your 'search' property to a non-recursive function type, it would also fix your issue ie search: (data: any) => void. But this would be a contrived and lazy solution because as I said, semantically, a function cannot be a value of a form.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM