1

I am trying to learn React and Typescript. I am building a little demo app that sends a request to the api https://api.zippopotam.us/us to get postcode information and display the place information. If the request is submitted with an invalid postcode I want to display an error message.

Below is my code. I have the call to the api inside the useEffect method and notice that this runs twice when the app is loaded i.e. before the user has entered a zipcode and clicked the search button so the api call is made without the zipcode and hence returns a 404, so the error code is always displayed on initial load.

I thought that the useEffect method should only get run when the zipSearch value changes i.e. when a user enters a zip and clicks enter. Although reading up on the useEffect method is seems it runs everytime the app component renders. Im also a little confused why it runs twice on initial load.

App with dev console on initial load

How can I get this to work the way I want it to? Any help would be much appreciated. If a moderator deletes this question, can they please let me know why? Thanks.

import React, {FormEvent, useEffect, useState} from "react";
import { useForm } from 'react-hook-form'
import "./App.css";
import axios from "axios";
import { IPlace } from "./IPlace";

export default function App2(){
    const [placeFound, setPlaceFound] = useState<IPlace[]>([]);
    const [zipSearch, setZipSearch] = useState("");
    const [errorFound, setErrorFound] = React.useState("");
  

    const renderPlaces = () => {
        console.log("Render places runs")

        if(placeFound.length !== 0){
            return (<div className="table-container">
            <table>
                <thead>
                    <tr>
                        <th><span>State</span></th>
                        <th><span>Longitude</span></th>
                        <th><span>Latitude</span></th>
                        <th><span>Place Name</span></th>
                    </tr>
                </thead>
                {placeFound.map((place) =>{
                return (
                    <tbody>
                        <tr>
                            <td>{place.state}</td>
                            <td>{place.longitude}</td>
                            <td>{place.latitude}</td>
                            <td>{place["place name"]}</td>
                        </tr>
                    </tbody>
                    )})}
            </table>
        </div>)
        }
        
        
    }

    React.useEffect(() => {
        console.log("useEffect is run")
        const query = encodeURIComponent(zipSearch);
        
        axios
          .get(`https://api.zippopotam.us/us/${query}`,{
          })
          .then((response) => {
            setPlaceFound(response.data.places);
            setErrorFound("");
          })
          .catch((ex) => {
            let errorFound = axios.isCancel(ex)
              ? 'Request Cancelled'
              : ex.code === 'ECONNABORTED'
              ? 'A timeout has occurred'
              : ex.response.status === 404
              ? 'Resource Not Found'
              : 'An unexpected error has occurred';
            setErrorFound(ex.code);
            setPlaceFound([]);
          });
          
        }
        
      },[zipSearch]);

    const search=(event: FormEvent<HTMLFormElement>) =>{
        console.log("Search method runs")
        event.preventDefault();
        const form = event.target as HTMLFormElement;
        const input = form.querySelector('#zipSearchInput') as HTMLInputElement;
        setZipSearch(input.value);
    }

    return (
        <div className="App">
            <div className="search-container">
                <h1>Place Search using Zip Code</h1>
                <form className="searchForm" onSubmit={event => search(event)}>
                    <div>
                        <label htmlFor="zipSearchInput">Zip Code</label>
                        <input {...register('zipSearchInput', { required: true, minLength: 5, maxLength: 5 }) }
                        id="zipSearchInput" 
                        name="zipSearchInput" 
                        type="text"
                        />
                    </div>                
                    {
                        errors.zipSearchInput && <div className="error">Zip Code is required and must be 5 digits long</div>
                    }
                    <button type="submit">Search</button>
                </form>
            </div>
            {placeFound.length !== 0 && renderPlaces()}
            {errorFound !== "" && <p className="error">{errorFound}</p>}
    </div>)
}

1 Answer 1

1

What you should probably do is actually use the library above react-hook-form and its functions.

Then I believe that the useEffect that you are using is pretty useless in this scenario, You are going the long way for a simpler task. You can simply call the api on submit of the form and get rid of the state zipSearch the react-hook-form is taking care of that for you.

Here's the fixed version of the code below:

import React, {  useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import axios from "axios";
interface IPlace {
  state: string;
  longitude: string;
  latitude: string;
  "place name": string;
}
export default function App2() {
  const [placeFound, setPlaceFound] = useState<IPlace[]>([]);
  const [errorFound, setErrorFound] = React.useState("");
  const formMethods = useForm<{ zipSearchInput: string }>();

  const renderPlaces = () => {
    console.log("Render places runs");

    if (placeFound.length !== 0) {
      return (
        <div className="table-container">
          <table>
            <thead>
              <tr>
                <th>
                  <span>State</span>
                </th>
                <th>
                  <span>Longitude</span>
                </th>
                <th>
                  <span>Latitude</span>
                </th>
                <th>
                  <span>Place Name</span>
                </th>
              </tr>
            </thead>
            {placeFound.map((place) => {
              console.log(place);
              
              return (
                <tbody key={place.latitude}>
                  <tr>
                    <td>{place.state}</td>
                    <td>{place.longitude}</td>
                    <td>{place.latitude}</td>
                    <td>{place["place name"]}</td>
                  </tr>
                </tbody>
              );
            })}
          </table>
        </div>
      );
    }
  };


  const search = (values: { zipSearchInput: string }) => {
    console.log(values);
    const query = encodeURIComponent(values.zipSearchInput);

    axios
      .get(`https://api.zippopotam.us/us/${query}`, {})
      .then((response) => {
        setPlaceFound(response.data.places);
        setErrorFound("");
      })
      .catch((ex) => {
        let _errorFound = axios.isCancel(ex)
          ? "Request Cancelled"
          : ex.code === "ECONNABORTED"
          ? "A timeout has occurred"
          : ex.response.status === 404
          ? "Resource Not Found"
          : "An unexpected error has occurred";
        setErrorFound(_errorFound);
        setPlaceFound([]);
      });
  };

  return (
    <div className="App">
      <div className="search-container">
        <h1>Place Search using Zip Code</h1>
        <FormProvider {...formMethods}>
          <form className="searchForm" onSubmit={formMethods.handleSubmit(search)}>
            <div>
              <label htmlFor="zipSearchInput">Zip Code</label>
              <input
                {...formMethods.register("zipSearchInput", {
                  required: true,
                  minLength: 5,
                  maxLength: 5
                })}
                id="zipSearchInput"
                name="zipSearchInput"
                type="text"
              />
            </div>
            <button type="submit">Search</button>
          </form>
        </FormProvider>
      </div>
      {placeFound.length !== 0 && renderPlaces()}
      {errorFound !== "" && <p className="error">{errorFound}</p>}
    </div>
  );
}

Good Luck

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks so much Nader. This works. I notice that for a valid zip that only one api call is made, which is good, but the renderPlaces method gets executed twice. This probably isn't too big of a deal, but ideally should it only be executing once? Also the input form validation doesn't really work. But this is a separate issue which I will try to solve myself. Thanks again.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.