DEV Community

Cover image for Create a Weather App with React Hooks: Part 2
Hulya
Hulya

Posted on • Edited on

Create a Weather App with React Hooks: Part 2

For the first part of the project, we have used useState hook, fetched our data and console logged the data for any city typed by the user.

Before we continue, we need to remove some of the code from the last part to be able to use our custom hook. Let's go inside our CitySelector component and remove onSearch function, also remove the results state. We will handle our button click from App.js, pass as a prop inside our Button component.

Now, my CitySelector.js is looking like this.

// components/CitySelector.js

import React, {useState} from 'react';
import {Row, Col, FormControl, Button} from 'react-bootstrap';

const CitySelector = ({onSearch}) => {
    const [city, setCity] = useState('');

    return (
      <>
        <Row>
          <Col>
            <h1>Search your city</h1>
          </Col>
        </Row>

        <Row>
          <Col xs={4}>
            <FormControl
              placeholder="Enter city"
              onChange={(event) => setCity(event.target.value)}
              value={city}
            />
          </Col>
        </Row>

        <Row>
          <Col>
           {/* don't forget to edit our function  */}
            <Button onClick={() => onSearch(city)}>Check Weather</Button>
          </Col>
        </Row>
      </>
    );
  };

export default CitySelector;

Enter fullscreen mode Exit fullscreen mode

Now, we will show the data in our UI and display 5 days of data. To be able to do that, we will use another hook named useEffect hook.

useEffect Hook

The useEffect Hook can help us to replace React lifecycle events. Lifecycle events are a set of events that take place at some point when a component is updated, changed, or removed. These are componentDidMount, componentDidUpdate, and componentWillUnmount. It's used for side effects (all things which happen outside of React) like network requests, managing subscriptions, DOM manipulation, setting up event listeners, timeouts, intervals, or local storage, etc.

Short Info About Lifecycle Events

  • ComponentDidMount is going to be called right after our component is added to the DOM. It fetches data at the initial render phase.
  • ComponentDidUpdate, updates the DOM when anything changes like state changes.
  • ComponentWillUnmount allows us to do any sort of cleanup. For example, if you want to invalidate a timer, or if you want to do any cleanup of any nodes, you can do that with this event. It runs right before the component is being removed from the web page.

How useEffect works?

  • useEffect listens for any change in our app.
  • It takes a function and two arguments.
  • First argument helps us tell useEffect when the code to be executed.
  • Second argument or the dependency array controls when the code gets executed. For the second argument, we can pass an array, an array with value/values, or no array at all.
    • If we don't pass an array, this will run only at initial render only once.
    • If we pass an empty array, this will run at initial render and whenever it rerenders.
    • If we pass an array with value/values inside of it, this will run at initial render and runs whenever our data changes inside the array.

Custom Hooks for Search

Create a new folder under src named hooks then create a new file named UseFetch.js.


// hooks/UseFetch.js

import {useState, useEffect} from 'react';

const UseFetch = (initialUrl) => {
  // create state variables
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [isLoading, setIsLoading] = useState(null);
  const [url, setUrl] = useState(initialUrl);

  useEffect(() => {

    setIsLoading(true);

    fetch(url)
      .then((response) => response.json())
      .then((data) => {
        setIsLoading(false);
        setData(data);
      })
      .catch((error) => {
        setIsLoading(false);
        setError(error);
      });
      // dependency array 
  }, [url]);

  return { data, error, isLoading, setUrl };
};

export default UseFetch;
Enter fullscreen mode Exit fullscreen mode

Generally, in our custom hooks, we place our logic like we can put our useState and useEffect hooks. As you can see, we have imported useEffect hook from React, and we have defined some useState variables.

For the useEffect hook, we have created an anonymous function. The most important part of our custom hook is return statement. Here, we return whatever we want another component to have access to. We can return an array or an object. If you return an array, we can name the returned values whatever we want outside the file. We don't need to keep the same name as we have returned.

Another side note is about url. We have to define a state hook because whenever our user searches for any city, our url will change. To keep track of its state, we added a state for that.

Also, you should take note of our useEffect dependency array. If we place some variables inside our array, our app will be updated whenever our URL changes. That's why we also return our setUrl function.

But we may have a problem here, when we first load our app, we may not have any url, for that we have added a conditional check.

Now, let's import this into our App.js component, and pass our custom hook. To do this, we can destructure our variables from UseFetch function.

import UseFetch from '../hooks/UseFetch';

With the help of our custom hook, we can call our API every time the button is clicked.

// App.js

import React from 'react';
import CitySelector from './components/CitySelector';
import './App.css';
import {Container} from 'react-bootstrap';
import UseFetch from './hooks/UseFetch'
import {API_KEY, API_BASE_URL} from './apis/config';

const App = () => {
  // destructure the returned values
  const {data, error, isLoading, setUrl} = UseFetch();

  return (
    <Container className="App">
        <CitySelector onSearch={(city) => setUrl(`${API_BASE_URL}/data/2.5/forecast?q=${city}&appid=${API_KEY}`)} />
    </Container>
  );
};

export default App;

Enter fullscreen mode Exit fullscreen mode

Now, we can fetch our data with useEffect custom hook. It prints null multiple times because we have different setters inside our custom hook.

use-effect.gif

Populate the Data

Now, let's populate our data and show 5 days weather data. For this, we will create another component. Under the components folder, create WeatherList.js component.


// components/WeatherList.js

import React from 'react'
import { Col, Row } from 'react-bootstrap'
import WeatherCard from './WeatherCard'

const WeatherList = ({weathers}) => {
    return (
        <Row>
           {weathers.map(({dt, main, weather}) => (
                <Col key={dt}>
                    <WeatherCard 
                    temp_max={main.temp_max} 
                    temp_min={main.temp_min} 
                    dt={dt * 1000} 
                    main={weather[0].main} 
                    icon={weather[0].icon} 
                  />
                </Col>
            ))} 
        </Row>
    )
}

export default WeatherList;
Enter fullscreen mode Exit fullscreen mode

Now, letโ€™s break down the code above to explain what weโ€™ve added and how it works.

  • We passed weathers prop and pass it from our App.js file.
  • For the jsx, we used Row and Col components from react-bootstrap.
  • To create columns, we mapped over our weathers array and populate 5 columns next to each other displaying weather data for 5 consecutive days.
  • Every column contains datetime, main and weather data from our API.
  • Nested our WeatherCard component inside the WeatherList component and pass its prop values from here.
  • As you may notice, we also passed the key property for our mapped weather cards. If we don't pass a key, React will complain about it. Because, when we map over an array, we need an identifier like an id.

Now, we can import our WeatherList component inside App.js. Here we need to render WeatherList conditionally, if we have data from our API, render the WeatherList component, also pass our prop named weathers to reach our API results.

import React from 'react';
import CitySelector from './components/CitySelector';
import './App.css';
import {Container} from 'react-bootstrap';
import UseFetch from './hooks/UseFetch';
import {API_KEY, API_BASE_URL} from './apis/config'
import WeatherList from './components/WeatherList';

const App = () => {
  const {data, error, isLoading, setUrl} = UseFetch();
  console.log(data);

  return (
    <Container className="App">
      <CitySelector onSearch={(city) => setUrl(`${API_BASE_URL}/data/2.5/forecast?q=${city}&cnt=5&appid=${API_KEY}`)} />

    {/* conditionally render  */}
      {data && <WeatherList weathers={data.list} />}
    </Container>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

results.png

If you had a problem with the styling, remove inline style (style={{width: '18rem'}}) from WeatherCard component.

Error Handling and Loading

As you may notice we didn't make use of isLoading and error variables yet.

For that, we will create multiple conditional checks before rendering our WeatherList component. If we pass all the checks we will display our WeatherList component.


// App.js
const App = () => {
  const {data, error, isLoading, setUrl} = UseFetch();

// error handling and loading
  const getContent = () => {
    if(error) return <h2>Error when fetching: {error}</h2>
    if(!data && isLoading) return <h2>LOADING...</h2>
    if(!data) return null;
    return <WeatherList weathers={data.list} />
  };

  return (
    <Container className="App">
      <CitySelector onSearch={(city) => setUrl(`${API_BASE_URL}/data/2.5/forecast?q=${city}&cnt=5&appid=${API_KEY}`)} />

      {/* don't forget the change */}
      {getContent()}
    </Container>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

If we make different searches, our search is not updating. To clear the previous searches from our state, we need to edit our UseFetch function.

If the user types sth other than a city, I mean if it doesn't exist in our API data, we get an error page. To fix that, we will make a check if we get a data.cod bigger than 400 we will show an error.


// hooks/UseFetch.js

import {useState, useEffect} from 'react';

const UseFetch = (initialUrl) => {
  // create state variables
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [isLoading, setIsLoading] = useState(null);
  const [url, setUrl] = useState(initialUrl);

  useEffect(() => {
    if(!url) return;
    setIsLoading(true);
    // clear old search
    setData(null);
    setError(null);

    fetch(url)
        .then((response) => response.json())
        .then((data) => {

            // error handling for nonexistent data
            setIsLoading(false);
            if(data.cod >= 400) {
                setError(data.message);
                return;
            }
            setData(data);
        })
        .catch((error) => {
            setIsLoading(false);
            setError(error);
        });
  }, [url]);

  return { data, error, isLoading, setUrl };
};

export default UseFetch;

Enter fullscreen mode Exit fullscreen mode

If we put sth other than a city, we get this message.

error.png

Wrapping Up

With this last touch to our app, we have completed all we need for this project. I hope you found it useful.

You can check out the React Official Page for a detailed explanation of the hooks. Also, you can deep dive into useEffect hook with this blog post from Dan Abramov.

You can find the source code here.

Thank you for reading and I hope you tried it yourself, too. This is my first experience as a writer, and it's actually really hard to follow where I was with the code and the tutorial. I hope I will get better soon ๐Ÿ˜ƒ.

Also, feel free to connect with me on Twitter, and Github.

Thanks for your time. Like this post? Consider buying me a coffee to support me writing more.

Top comments (4)

Collapse
 
timlangner profile image
Tim Langner

Really cool and well explained tutorial! But didn't you want to show data/cards for the next 5 days? It only shows the weather every 3 hours five times.

Collapse
 
hulyamasharipov profile image
Hulya • Edited

Hi, thanks for your feedback! I just checked the API, this API is for calling 5 days / 3-hour forecast data. You can find the info here. I didn't notice until you told me. Thanks again ๐Ÿ˜ƒ

Collapse
 
yashp1 profile image
Yash Patel

Thank you! It was a nice tutorial to understand React Hooks ^_^

Collapse
 
hulyamasharipov profile image
Hulya

Thanks ๐Ÿ˜Š Glad it helped you to understand