DEV Community

Cover image for React-Native-Weather App 🌦
Joao Hortale
Joao Hortale

Posted on

React-Native-Weather App 🌦

Project

Hello guys, this tutorial is for the weather forecast application that I developed in React-Native using geolocation, a OpenWeather API and data persisting in case the network signal goes down.

I built this project using Expo and it is quite simple although it requires some libs and APIs like React useState and useEffect to manage the custom Hooks, Axios to fetch weather API, AsyncStorage, and Styled-Component.

Code

/utils

Geolocation

I started this project using this geolocation React-Native method to get the coordinates to send to the weather api later. I created a Hook in order to keep tracking this information in case the geoLocation changes and updates my state.

useGeolocation.js

import { useState, useEffect } from "react";

export default function useGeoLocation(lat, lon) {
  const [latLon, setLatLon] = useState(null);

  useEffect(() => {
    navigator.geolocation.getCurrentPosition(
      (position) => {
        setLatLon([position.coords.latitude, position.coords.longitude]);
      },
      (err) => {
        console.log(err);
      }
    );
  }, []);

  return latLon;
}


js

Weather API

After receiving the geolocation coordinates, I send this data to the API and receive the call back with all weather data. As soon as I receive this response, I need to store the data in cache.

useWeather.js

import { useState, useEffect } from "react";
import { API_KEY } from "react-native-dotenv";
import axios from "axios";
import { storeWeather, getWeather } from "./storeWeather";
import useGeoLocation from "./useGeoLocation";

// fetch api with axios
const url = "https://api.openweathermap.org/data/2.5";

const callAPI = axios.create({
  baseURL: url,
  timeout: 1000,
});

export default function useWeather(lat, lon) {
  const [weather, setWeather] = useState(null);

  const latLon = useGeoLocation();

  useEffect(() => {
    if (latLon) {
      fetchAPI(...latLon);
    }
  }, [latLon]);

  const fetchAPI = async (lat, lon) => {
    try {
      const endpoint = `/forecast?lat=${lat}&lon=${lon}&units=metric&appid=${API_KEY}`;
      const res = await callAPI.get(endpoint);
      const data = await storeWeather(filterData(res.data));

The state is always fed by the AsyncStorage to make sure that we will always get some data in case the internet connection or gps is down.

      setWeather(data);
    } catch (err) {
      console.log("API conection failed");
      const data = await getWeather();
      setWeather(data);
    }
  };

  return weather;
}

const filterData = (rawData) => {
  return {
    id: rawData.city.id,
    name: rawData.city.name,
    country: rawData.city.country,
    timezone: rawData.city.timezone,
    coord: {
      lat: rawData.city.coord.lat,
      lon: rawData.city.coord.lon,
    },
    list: rawData.list,
  };
};

Data Pesistence

There are two functions to handle data here. One to store and another to retrieve it. This could also be made with SQLite or any other database for mobile. AsyncStorage only saves the string type, so I have to convert data to string to record or parse it when I retrieve it.

storeWeather.js

import { AsyncStorage } from "react-native";

// connect Database
export const storeWeather = async (value) => {
    try {
        const jsonValue = JSON.stringify(value);
        await AsyncStorage.setItem("@storage_Key", jsonValue);
        console.log("Data Pesisted in Cache");
        return jsonValue != null ? JSON.parse(jsonValue) : null;
    } catch (err) {
        console.log(err);
    }
};

export const getWeather = async () => {
    try {
        const jsonValue = await AsyncStorage.getItem("@storage_Key");
        jsonValue === undefined
            ? console.log("No Data in Cache")
            : console.log("Retrieved from Cache");
        return jsonValue != null ? JSON.parse(jsonValue) : null;
    } catch (err) {
        console.log(err);
    }
};

imageDictionary.js

export default {
    "01d": require("../assets/icons/01d.png"),
    "01n": require("../assets/icons/01n.png"),
    "02d": require("../assets/icons/02d.png"),
    "02n": require("../assets/icons/02n.png"),
    "03d": require("../assets/icons/03d.png"),
    "03n": require("../assets/icons/03n.png"),
    "04n": require("../assets/icons/04n.png"),
    "10d": require("../assets/icons/10d.png"),
    "10n": require("../assets/icons/10n.png"),
}

I used an icon collection and I created a dictionary to match every weather condition with the right icon. I did not cover all the conditions, but feel free if you want to correlate everything.

/components

Basically I made the UI using Styled components and Flaticon library.

Loading.js

import React from "react";
import imageDictionary from "../utils/imageDictionary.js";
import { Container, BigText, BigIcon, Description } from "./Styles";

const Loading = (props) => {
  return (
    <Container>
      <BigText>Welcome!</BigText>
      <BigIcon source={imageDictionary["01d"]} />
      <Description>Loading...</Description>
    </Container>
  );
};
export default Loading;

While API is fetching data, this loading screen shows up.

App.js

import React from "react";
import useWeather from "./utils/useWeather";
import Loading from "./components/Loading";
import Weather from "./components/Weather";
import { Container } from "./components/Styles";

export default function App() {
  const weather = useWeather();
  return (
    <Container>
      {!weather ? <Loading /> : <Weather forecast={weather} />}
    </Container>
  );
}

The App.js component is very simple. It only imports all the dependencies and formats the main view with styled-component.

Styled Components

I created a file to keep all my "classes" together and I import them whenever needed.

Styles.js

import styled from "styled-components/native";

export const Container = styled.SafeAreaView`
  flex: 1;
  background-color: #272343;
  justify-content: center;
  width: 100%;
  align-items: center;
`;
export const CurrentDay = styled.View`
  position: relative;
  flex: 1;
  margin-top: 60px;
  align-items: center;
`;

export const City = styled.Text`
  font-size: 22px;
  font-weight: 300;
  color: white;
  padding-bottom: 20px;
`;
export const BigText = styled.Text`
  font-size: 35px;
  font-weight: 100;
  color: white;
  padding-bottom: 20px;
`;

export const BigIcon = styled.Image`
  width: 168px;
  height: 168px;
  padding-bottom: 40px;
`;

export const Temp = styled.Text`
  font-size: 80px;
  font-weight: 100;
  color: #bae8e8;
`;
export const Description = styled.Text`
  font-size: 24px;
  font-weight: 100;
  color: #bae8e8;
  padding-top: 20px;
`;

export const Week = styled.ScrollView`
  bottom: 0;
  left: 0;
  width: 100%;
  height: 150px;
  position: absolute;
  background: black;
`;

export const Day = styled.View`
  height: 150px;
  width: 75px;
  justify-content: center;
  align-items: center;
`;

export const SmallIcon = styled.Image`
  width: 50px;
  height: 50px;
`;
export const SmallText = styled.Text`
  font-size: 20px;
  font-weight: 300;
  color: white;
`;

Weather

Working with dates is always confusing, mainly when you have to deal with different timezones. To make my work less complicated I imported another lib date-fns

Weather.jsx

import React from "react";
import { isSameDay, format } from "date-fns";
import imageDictionary from "../utils/imageDictionary.js";
import Card from "./Card";
import {
    Container,
    CurrentDay,
    City,
    BigText,
    BigIcon,
    Temp,
    Description,
    Week,
} from "./Styles";

const Weather = ({ forecast: { name, list, timezone } }) => {

This block code below is responsible for the current weather. The API callback response is an array with a 7-day forecast data, updated every 3 hours with new conditions, so to get the most recent forecast I always get the first position of the array.

    const currentWeather = list.filter((day) => {
        const now = new Date().getTime() + Math.abs(timezone * 1000);
        const currentDate = new Date(day.dt * 1000);
        return isSameDay(now, currentDate);
    });

This second block is responsible for the next forecast. It is another array whose date is already pre-formatted to be sent to the child component at the bottom.

    const daysByHour = list.map((day) => {
        const dt = new Date(day.dt * 1000);
        return {
            date: dt,
            hour: dt.getHours(),
            name: format(dt, "EEEE"),
            temp: Math.round(day.main.temp),
            icon:
                imageDictionary[day.weather[0].icon] || imageDictionary["02d"],
        };
    });

    return (
        currentWeather.length > 0 && (
            <Container>
                <CurrentDay>
                    <City>{name}</City>
                    <BigText>Today</BigText>
                    <BigIcon
                        source={
                            imageDictionary[
                                currentWeather[0].weather[0].icon
                            ] || imageDictionary["02d"]
                        }
                    />
                    <Temp>{Math.round(currentWeather[0].main.temp)}Β°C</Temp>
                    <Description>
                        {currentWeather[0].weather[0].description}
                    </Description>
                </CurrentDay>
                <Week horizontal={true} showsHorizontalScrollIndicator={false}>
                    {daysByHour.map((day, index) => (
                        <Card
                            key={index}
                            icon={day.icon}
                            name={day.name.substring(0, 3)}
                            temp={day.temp}
                            hour={day.hour}
                        />
                    ))}
                </Week>
            </Container>
        )
    );
};
export default Weather;

Cards

This card only receives data sent from the array and it creates a horizontal scrolling view with the list of forecast for the next 7 days.

Card.js

import React from "react";
import { Day, SmallIcon, SmallText } from "./Styles";

export default function Card({ name, icon, temp, hour }) {
  return (
    <Day>
      <SmallIcon source={icon} />
      <SmallText>{name}</SmallText>
      <SmallText>{temp}Β°C</SmallText>
      <SmallText>{hour}h</SmallText>
    </Day>
  );
}

That's all, folks! I hope you guys liked the way that I did this project and I would love to hear some feedback from you. Also feel free to fork it and contribute. There are several features that we can add to it... This is my Git repository with the full project.

Peace!

Joao

Top comments (2)

Collapse
 
drukey profile image
Drukey

Hi thanks, such a great tutorial. What if I need to display the current weather of other cities, like not based on my current location but for other cities.

Collapse
 
jhortale profile image
Joao Hortale

Probably you will need to add a search input to find other cities send its coordinates to weather api.