Creating and testing a Simple Weather App in ReactJS: A Step-by-Step Guide

Table of contents

No heading

No headings in the article.

In this article, we will show you how to create and test a simple weather application in ReactJS using Vite and TypeScript instead of create-react-app. The weather application will consist of a form where the user can enter the name of a city and a button to submit the form. When the form is submitted, the application will make a request to the OpenWeatherMap API to retrieve the current weather data for the specified city. The weather data will be displayed on the page, including the city name, temperature, and description of the weather conditions.

To use the OpenWeatherMap API, we will need an API key. You can create a free account on the OpenWeatherMap website to get your own API key, or you can sign up for a paid plan to access additional features and higher usage limits. The API documentation can be found at http://api.openweathermap.org/.

To improve the performance of the application, we will use the useMemo and useCallback hooks to memoize expensive calculations and avoid unnecessary re-renders of components. We will also use the useRef hook to create a reference to the city input element and avoid re-creating it on every render.

In addition, we will use the lodash library to debounce the form submission

Let’s get started! 👌

First, we will create a new project using Vite, which is a lightweight and fast alternative to create-react-app. To create a new project, open your terminal and run the following commands:

npm create vite@latest weather-app -- --template react-ts

Next, we will install the axios library, which we will use to make HTTP requests to the OpenWeatherMap API. We will also install the lodash.debounce function, which we will use to debounce the API requests to avoid making too many requests in a short amount of time.

npm install axios lodash.debounce

Now that we have our project set up, we can start building our weather application. We will start by creating a simple form that allows users to enter the name of the city they want to get the weather data for.

In the src directory of your project, create a new file called WeatherForm.tsx and add the following code:

import React, { useCallback } from "react"

const WeatherForm: React.FC<{ onSubmit: (city: string) => void }> = ({ onSubmit }) => {
  const [city, setCity] = React.useState("");
  const [error, setError] = React.useState("");

  const onChange = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      setCity(event.target.value);
    },
    []
  );

  const onSubmitForm = useCallback(
    (event: React.FormEvent<HTMLFormElement>) => {
      event.preventDefault();
      if (!city) {
        setError("Please enter a city");
      } else {
        onSubmit(city);
      }
    },
    [city, onSubmit]
  );

  return (
    <form onSubmit={onSubmitForm}>
      <label htmlFor="city">City:</label>
      <input type="text" id="city" name="city" value={city} onChange={onChange} />
      {error && <p>Error: {error}</p>}
      <button type="submit">Get Weather</button>
    </form>
  );
};

export default WeatherForm;

This component is a simple form that allows users to enter the name of the city they want to get the weather data for. We have used the useCallback hook to create memoized versions of the onChange and onSubmitForm functions. This means that these functions will only be re-created if one of the dependencies in the second argument to useCallback changes.

By using the useCallback hook, we can avoid unnecessary re-creations of the onChange and onSubmitForm functions, which can improve the performance of the WeatherForm component and the overall application.

onChange and onSubmitForm functions in the WeatherForm component will only be re-created if their dependencies change, which can improve the performance of the component and the overall application.

Next, we will create a component that displays the weather data for a given city. Create a new file called WeatherData.tsx and add the following code:

import React from "react";

interface WeatherDataProps {
  city: string;
  temperature: number;
  description: string;
}

const WeatherData: React.FC<WeatherDataProps> = ({ city, temperature, description }) => {
  return (
    <div>
      <h1>Weather in {city}:</h1>
      <p>Temperature: {temperature}</p>
      <p>Description: {description}</p>
    </div>
  );
};

export default WeatherData;

This component takes the city, temperature, and description of the weather as props and displays them in a simple format.

Now that we have our form and data display components, we can create our main weather component that brings everything together. Create a new file called Weather.tsx and add the following code:

import React from "react";
import axios from "axios";
import WeatherForm from "./WeatherForm";
import WeatherData from "./WeatherData";
import debounce from "lodash.debounce";

const API_KEY = process.env.API_KEY;
const API_ENDPOINT = `https://api.openweather?appid=${API_KEY}`;

const Weather: React.FC = () => {
  const [city, setCity] = React.useState("");
  const [temperature, setTemperature] = React.useState(0);
  const [description, setDescription] = React.useState("");
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState("");

   const fetchWeatherData = useCallback(async (city: string) => {
    setLoading(true);
    try {
      const response = await axios.get(`${API_ENDPOINT}&q=${city}`);
      const data = response.data;
      const { main, weather } = data;
      setCity(city);
      setTemperature(main.temp);
      setDescription(weather[0].description);
    } catch (err) {
      setError(err.message);
    }
    setLoading(false);
  }, []);

  const debouncedFetchWeatherData = useMemo(() => debounce(fetchWeatherData, 500), [
    fetchWeatherData,
  ]);

  return (
    <div>
      <h1>Weather App</h1>
      {loading && <p>Loading...</p>}
      {error && <p>Error: {error}</p>}
      <WeatherForm onSubmit={debouncedFetchWeatherData} />
      {city && <WeatherData city={city} temperature={temperature} description={description} />}
    </div>
  );
};

export default Weather;

In Weather.tsx component, we have used the useCallback hook to create a memoized version of the fetchWeatherData function. This means that the function will only be re-created if one of the dependencies in the second argument to useCallback changes.

We have also used the useMemo hook to create a memoized version of the debounce function that is applied to the fetchWeatherData function. This means that the debounce function will only be re-created if the fetchWeatherData function changes, which will happen if one of its dependencies changes.

By using these hooks, we can avoid unnecessary re-creations of the fetchWeatherData and debounce functions, which can improve the performance of the application.

fetchWeatherData and debounce functions will only be re-created if their dependencies change, which can improve the performance of the application.

You will need to add the following lines to your .env file:

API_KEY=your_api_key

Be sure to replace your_api_key with the actual value for your API key.

Note that the .env file should not be committed to version control. It should be used only for local development, and the values should be different in production.

Testing

To write unit tests for the weather application, we can use a testing framework like jest and @testing-library/react. We can start by writing a test for the WeatherForm component to ensure that it correctly handles user input and calls the onSubmit callback when the form is submitted.

First, we need to add the jest and @testing-library/react dependencies to our project. We can do this by running the following command:

npm install --save-dev jest @testing-library/react

Next, we can create a __tests__ directory in the root of our project and add a WeatherForm.test.tsx file inside it. This file will contain our test for the WeatherForm component.

import React from "react";
import { render, fireEvent } from "@testing-library/react";
import WeatherForm from "../WeatherForm";

describe("WeatherForm", () => {
  it("should call the onSubmit callback when the form is submitted", () => {
    const onSubmit = jest.fn();
    const { getByLabelText, getByText } = render(<WeatherForm onSubmit={onSubmit} />);
    const cityInput = getByLabelText(/city/i) as HTMLInputElement;
    const submitButton = getByText(/get weather/i);
    fireEvent.change(cityInput, { target: { value: "London" } });
    fireEvent.click(submitButton);
    expect(onSubmit).toHaveBeenCalledWith("London");
  });
});

In this test, we are using the render function from @testing-library/react to render the WeatherForm component. We are then using the getByLabelText and getByText functions to get a reference to the city input and the submit button in the form.

We are using the fireEvent function to simulate user interactions with the form by changing the value of the city input and clicking the submit button.

Finally, we are using expect and the toHaveBeenCalledWith matcher from jest to verify that the onSubmit callback was called with the correct city name when the form was submitted.

After writing this test, we can run it by using the jest command:

jest

If the test passes, we should see a message like this:

PASS  __tests__/WeatherForm.test.tsx
  WeatherForm
    ✓ should call the onSubmit callback when the form is submitted (5ms)
    Test Suites: 1 passed, 1 total
    Tests:       1 passed, 1 total
    Snapshots:   0 total
    Time:        2.849s
    Ran all test suites.

We can write a test for the WeatherData component to ensure that it correctly displays the city, temperature, and description data passed to it as props.

Add the following test to the WeatherForm.test.tsx file:

it("should display the city, temperature, and description data", () => {
  const { getByText } = render(
    <WeatherData
      city="London"
      temperature={20}
      description="Mostly cloudy"
    />
  );

  expect(getByText(/london/i)).toBeInTheDocument();
  expect(getByText(/20/i)).toBeInTheDocument();
  expect(getByText(/mostly cloudy/i)).toBeInTheDocument();
});

In this test, we are using the render function from @testing-library/react to render the WeatherData component with some sample data. We are then using the getByText function to get a reference to the city, temperature, and description elements in the component and using the toBeInTheDocument matcher from jest to verify that they are correctly displayed.

After writing this test, we can run it by using the jest command:

jest

If the test passes, we should see a message like this:

PASS  __tests__/WeatherForm.test.tsx
  WeatherForm
    ✓ should call the onSubmit callback when the form is submitted (5ms)
    ✓ should display the city, temperature, and description data (5ms)
    Test Suites: 1 passed, 1 total
    Tests:       2 passed, 2 total
    Snapshots:   0 total
    Time:        3.371s
    Ran all test suites.

We can continue to write additional tests for the weather application to ensure that all of its components are working correctly. By writing unit tests, we can catch bugs and errors early in the development process and ensure that our application is working as expected.

Refactoring

To further improve the performance of the WeatherForm.tsx component, we can use the useRef hook to create a reference to the city input element. This will avoid re-creating the input element every time the component is rendered, which can improve the performance of the component and the overall application.

Update the WeatherForm.tsx file with the following code:

import React from "react";

const WeatherForm: React.FC<{ onSubmit: (city: string) => void }> = ({ onSubmit }) => {
  const [error, setError] = React.useState("");
  const cityInputRef = React.useRef<HTMLInputElement>(null);

  const onSubmitForm = useCallback(
    (event: React.FormEvent<HTMLFormElement>) => {
      event.preventDefault();
      if (!cityInputRef.current?.value) {
        setError("Please enter a city");
      } else {
        onSubmit(cityInputRef.current.value);
      }
    },
    [cityInputRef, onSubmit]
  );

  return (
    <form onSubmit={onSubmitForm}>
      <label htmlFor="city">City:</label>
      <input type="text" id="city" name="city" ref={cityInputRef} />
      {error && <p>Error: {error}</p>}
      <button type="submit">Get Weather</button>
    </form>
  );
};

export default WeatherForm;

In this updated version of the WeatherForm.tsx component, we are using the useRef hook to create a reference to the city input element. We are then using this reference in the onSubmitForm function to access the value of the city input without re-creating the input element every time the component is rendered.

By using the useRef hook, we can avoid unnecessary re-creations of the city input element, which can improve the performance of the WeatherForm component and the overall application.

After making these changes, our weather application should continue to work as before, but the WeatherForm component will be more efficient and will use the useRef hook to create a reference to the city input element.

NOTE: Using the useMemo and useCallback hooks on every component and function can increase the complexity of the code and make it more difficult to understand and maintain. It is important to use these hooks wisely and only on those components, functions, and values that truly benefit from memoization or callback caching.

Overuse of the useMemo and useCallback hooks can lead to unnecessary complexity and can even negatively impact the performance of the application if the memoized values or callbacks are not used frequently enough.

In general, it is best to use the useMemo and useCallback hooks only when there is a clear benefit to memoizing a value or caching a callback, such as avoiding expensive calculations or re-creations of complex objects or avoiding unnecessary re-renders of components. Otherwise, it is better to avoid using these hooks and let the React framework manage the component rendering and optimization process.

Thank you for reading my article. I hope you found it informative and helpful. If you have any questions or feedback, please feel free to contact me. I would also be grateful if you would share this article with others who may be interested. Thank you again for your time and attention.