DEV Community

robin okwanma
robin okwanma

Posted on

Build a Weather App with React Native, OpenCageData, and OpenWeatherMap

Ready to build a real-world weather app? Let's dive in! We'll use the powerful combination of React Native Expo, OpenWeatherMap, and OpenCageData to create a mobile app that delivers accurate and up-to-date weather information.

First, sign up for a free OpenCageData account. This service is a game-changer, allowing us to fetch location data directly in our browser using its API URL. Once you've signed up, grab your API key and store it securely.

Next, head over to OpenWeatherMap and create a free account. This platform provides a wealth of weather data that we'll use to power our app. After signing up, retrieve your API key and keep it handy.

With these essential tools in place, we're ready to start building our weather app.

Let's get started!
If you're new to coding, make sure you have these tools ready:

  1. Node.js: Download and install it from the official website.

  2. Expo CLI: Install it globally using this command in your terminal:
    npm install -g expo-cli

  3. Expo App: Download the Expo app on your phone or tablet to test your app as you build it.

Step 1- Create a New Expo Project

Open your terminal and run the following command to create a new project named "WeatherHunt":

expo init WeatherHunt
cd WeatherHunt

Enter fullscreen mode Exit fullscreen mode

Step 2- Install Essential Tools

npm install axios react-native-elements react-native-dotenv

Enter fullscreen mode Exit fullscreen mode

Step 3: Configure Environment Variables

We'll use a .env file to store our API keys and URLs securely. To make this work, we need to configure Babel:

  • Open the babel.config.json file in your project.

  • Add the following plugin to the plugins array:

module.exports = function(api) {
    api.cache(true);
    return {
        presets: ['babel-preset-expo'],
        plugins: [
            'react-native-reanimated/plugin', ['module:react-native-dotenv', {
                moduleName: '@env',
                path: '.env',
            }],
        ],
    };
};
Enter fullscreen mode Exit fullscreen mode

Now you're all set to start building your weather app!

Step 4- Organize our project!

To keep our code clean and easy to understand, we're going to create three new folders:

  1. screens: This is where we'll put the main screen of our app, WeatherHunt.jsx. This screen is where users will search for cities and see the weather.

  2. config: This folder will hold the AccountService.jsx file. This file is like a secret agent, handling all the behind-the-scenes work of talking to the OpenCageData and OpenWeatherMap APIs.

  3. splashscreen: This folder will contain the SplashScreen.jsx file. It's the first thing users see when they open the app, giving a warm welcome while the app gets ready.

This way, we're keeping our UI (the stuff users see) separate from the backend logic (the stuff that makes things work). This makes our code easier to manage and update.

Here's a quick look at how our project will be structured:

WeatherApp/
├── config/
│   └── AccountService.jsx
├── screens/
│   └── WeatherHunt.jsx
├── splashscreen/
│   └── SplashScreen.jsx
├── App.js
├── package.json
├── .expo/
├── node_modules/
└── assets/
Enter fullscreen mode Exit fullscreen mode

By keeping things organized like this, we're setting ourselves up for success!

**

Step 5- Set Up the .env file

**

To keep your API URLs and tokens secure and easily configurable, we’ll use a .env file. Create a new file named .env in the root of your project and define your variables as shown below. Replace the dummy tokens with your actual API keys from the accounts you set up earlier.

API_URL= https://api.openweathermap.org/data/2.5/weather
API_TOKEN= dummyopenweathermaptoken
GEOCODING_API_URL = https://api.opencagedata.com/geocode/v1/json
GEOCODING_API_TOKEN= dummygeocodingapitoken

Enter fullscreen mode Exit fullscreen mode

By using a .env file, you can separate sensitive data from your codebase, making it easier to manage and secure.

**

Step 6- Create SplashScreen

**
Let's add a splash screen to give our app a little pizzazz! A splash screen is like a quick hello, a brief moment to greet your users while the app gets ready.

We'll create a new file called SplashScreen.jsx and put it in the splashscreen folder. This screen will show a short welcome message to keep users entertained while the app loads. Here’s the code for the splash screen:

import { View, Text, StyleSheet } from 'react-native';
import React from 'react';

const SplashScreen = () => {
  return (
    <View style={styles.container}>
      <Text style={styles.header}>WEATHER HUNTER</Text>
      <Text style={styles.description}>Discover the weather for any city or zip code in the world!</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#003366',
    alignItems: 'center',
    justifyContent: 'center',
  },
  header: {
    fontSize: 32,
    fontWeight: 'bold',
    color: '#ffffff',
    marginBottom: 20,
    textAlign: 'center',
  },
  description: {
    fontSize: 18,
    color: '#cccccc',
    textAlign: 'center',
    paddingHorizontal: 20,
  },
});

export default SplashScreen;

Enter fullscreen mode Exit fullscreen mode

Step 7- Make API Calls in AccountService.jsx

Next, we’ll create a service class to handle API calls. This class centralizes logic for fetching weather data and geocoding coordinates, keeping the main application code cleaner and more manageable. Below is the AccountService.jsx file:

class AccountService {
  constructor() {
    // OpenWeatherMap API details
    this.baseUrl = process.env.API_URL; // Base URL for weather data
    this.baseToken = process.env.API_TOKEN; // API key for OpenWeatherMap

    // OpenCage API details
    this.geocodingBaseUrl = process.env.GEOCODING_API_URL; // Base URL for geocoding
    this.geocodingApiToken = process.env.GEOCODING_API_TOKEN; // API key for OpenCage
  }

  // Fetch coordinates for a given location (city or zip code)
  async getCoordinates(location) {
    const url = `${this.geocodingBaseUrl}?q=${encodeURIComponent(location)}&key=${this.geocodingApiToken}`;
    const response = await fetch(url);
    if (!response.ok) throw new Error(`Error fetching coordinates: ${response.statusText}`);
    const data = await response.json();
    if (data.results.length === 0) throw new Error("Location not found");
    const { lat, lng } = data.results[0].geometry;
    return { lat, lon: lng };
  }

  // Get city suggestions based on user input
  async getCitySuggestions(query) {
    const url = `${this.geocodingBaseUrl}?q=${encodeURIComponent(query)}&key=${this.geocodingApiToken}`;
    const response = await fetch(url);
    if (!response.ok) throw new Error(`Error fetching city suggestions: ${response.statusText}`);
    const data = await response.json();
    return Array.isArray(data.results)
      ? data.results.map(result => ({
          name: result.formatted,
          lat: result.geometry.lat,
          lon: result.geometry.lng,
        }))
      : [];
  }

  // Fetch weather data using latitude and longitude
  async getWeather(lat, lon) {
    const url = `${this.baseUrl}?lat=${lat}&lon=${lon}&appid=${this.baseToken}&units=metric`;
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`Error fetching weather data: ${response.statusText}`);
      const data = await response.json();
      return data;
    } catch (error) {
      console.error("Error fetching weather data:", error);
      throw error;
    }
  }
}

export default new AccountService();

Enter fullscreen mode Exit fullscreen mode

Explanation of Methods

  • getCoordinates(location): Takes a location name or zip code as input. Fetches latitude and longitude using the OpenCage API.

  • getCitySuggestions(query): Takes a partial or full city name as input. Returns a list of possible matching cities with their coordinates.

  • getWeather(lat, lon): Uses latitude and longitude to fetch weather details from the OpenWeatherMap API.

By encapsulating API logic in AccountService, we ensure clean and reusable code, making it easier to maintain and debug.

**

Setp 8- Build the Main Interface Screen

**
Now that we’ve set up the splash screen and the API logic, it’s time to create the main interface of our weather application. This is where users will interact with the app to search for locations and view weather information. The interface includes a search bar for entering a city or zip code, a button to fetch weather data, and a results section to display the fetched weather details.

Create a new file named WeatherHunt.jsx inside the screens folder and add the following code:

import React, { useState } from "react";
import {
  View,
  Text,
  ActivityIndicator,
  StyleSheet,
  Modal,
  Image,
  TouchableOpacity,
  KeyboardAvoidingView,
  Platform,
  TouchableWithoutFeedback,
  Keyboard,
} from "react-native";
import AccountService from "../config/AccountService";
import { Input, Button } from "react-native-elements";

const WeatherHunt = () => {
  const [location, setLocation] = useState("");
  const [weather, setWeather] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [suggestions, setSuggestions] = useState([]);
  const [isModalVisible, setModalVisible] = useState(false);
  const [selectedSuggestion, setSelectedSuggestion] = useState(null);

  const handleSearch = async () => {
    setLoading(true);
    setError(null);
    setSuggestions([]);

    try {
      const data = await AccountService.getCitySuggestions(location);
      setSuggestions(data);
      setModalVisible(true);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const handleSelectSuggestion = (suggestion) => {
    setLocation(suggestion.name);
    setSelectedSuggestion(suggestion);
    setModalVisible(false);
  };

  const handleSubmit = async () => {
    setLoading(true);
    setError(null);
    setWeather(null);

    try {
      let weatherData;
      if (selectedSuggestion) {
        weatherData = await AccountService.getWeather(
          selectedSuggestion.lat,
          selectedSuggestion.lon
        );
      } else {
        const coordinates = await AccountService.getCoordinates(location);
        weatherData = await AccountService.getWeather(
          coordinates.lat,
          coordinates.lon
        );
      }
      setWeather(weatherData);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };
  // console.log(weather)

  return (
    <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
      <View style={styles.container}>
        <KeyboardAvoidingView behavior={Platform.OS === "ios" ? "padding" : null}>
          <Text style={styles.header}>WEATHER HUNTER</Text>
          <Text style={styles.description}>
            Enter a city or zip code to find the weather information.
          </Text>
          <View style={styles.searchContainer}>
            <Input
              placeholder="Enter city or zip code"
              value={location}
              onChangeText={(value) => {
                setLocation(value);
                setSelectedSuggestion(null);
              }}
              containerStyle={styles.inputContainer}
              inputContainerStyle={styles.input}
            />
            <View style={styles.buttonContainer}>
              <Button
                title="Search"
                onPress={handleSearch}
                buttonStyle={styles.button}
              />
              <Button
                title="Get Weather"
                onPress={handleSubmit}
                buttonStyle={styles.button}
              />
            </View>
          </View>
          <Modal visible={isModalVisible} transparent={true} animationType="slide">
            <View style={styles.modalContainer}>
              <View style={styles.modalContent}>
                {loading && <ActivityIndicator size="large" color="#0000ff" />}
                {suggestions.map((item, index) => (
                  <TouchableOpacity
                    key={index}
                    onPress={() => handleSelectSuggestion(item)}
                  >
                    <Text style={styles.suggestionItem}>{item.name}</Text>
                  </TouchableOpacity>
                ))}
                <Button title="Close" onPress={() => setModalVisible(false)} />
              </View>
            </View>
          </Modal>
          {loading && <ActivityIndicator size="large" color="#0000ff" />}
          {error && <Text style={styles.error}>{error}</Text>}
          {weather && (
            <View style={styles.weatherInfo}>
              <Text style={styles.weatherTitle}>{weather.name}</Text>
              <Text style={styles.weatherDescription}>
                {weather.weather[0]?.description || "No description available"}
              </Text>
              <Text style={styles.weatherTemp}>{weather.main.temp} °C</Text>
              <Image
                style={styles.weatherIcon}
                source={{
                  uri: `http://openweathermap.org/img/wn/${weather.weather[0].icon}@2x.png`,
                }}
              />
            </View>
          )}
        </KeyboardAvoidingView>
      </View>
    </TouchableWithoutFeedback>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    backgroundColor: "#f0f8ff",
    alignItems: "center",
    padding: 20,
  },
  header: {
    fontSize: 26,
    fontWeight: "bold",
    color: "#0059b3",
    marginBottom: 20,
    textAlign: "center",
  },
  description: {
    fontSize: 16,
    color: "#333",
    textAlign: "center",
    marginBottom: 20,
  },
  searchContainer: {
    alignItems: "center",
    justifyContent: "center",
    width: "100%",
  },
  inputContainer: {
    width: 300,
  },
  input: {
    borderColor: "#0059b3",
    borderWidth: 1,
    borderRadius: 8,
    paddingHorizontal: 10,
    backgroundColor: "#fff",
  },
  buttonContainer: {
    flexDirection: "row",
    justifyContent: "space-between",
    width: "100%",
    marginTop: 10,
  },
  button: {
    backgroundColor: "#0059b3",
    borderRadius: 8,
    width: 140,
  },
  modalContainer: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "rgba(0,0,0,0.5)",
  },
  modalContent: {
    width: "80%",
    backgroundColor: "white",
    padding: 16,
    borderRadius: 8,
  },
  suggestionItem: {
    padding: 10,
    borderBottomWidth: 1,
    borderBottomColor: "#ccc",
  },
  error: {
    color: "red",
    marginVertical: 10,
  },
  weatherInfo: {
    marginTop: 20,
    backgroundColor: "#fff",
    borderRadius: 15,
    padding: 30,
    alignItems: "center",
  },
  weatherTitle: {
    fontSize: 24,
    fontWeight: "bold",
    marginBottom: 10,
  },
  weatherDescription: {
    fontSize: 18,
    textTransform: "capitalize",
  },
  weatherTemp: {
    fontSize: 24,
    fontWeight: "bold",
    color: "#0059b3",
    marginBottom: 10,
  },
  weatherIcon: {
    width: 100,
    height: 100,
  },
});

export default WeatherHunt;

Enter fullscreen mode Exit fullscreen mode

Explanation:
Search Bar: Users can input a city or zip code to find weather information.

"Search" and "Get Weather" Buttons:
Search: Opens a modal with location suggestions based on the Zip code or text. This is useful if different places have the same name or code.
Get Weather: Fetches weather data directly if a specific location is selected or entered.
Weather Information Display: Displays temperature, description, and weather icon when data is fetched.
This step connects the UI with the backend logic, making the app interactive and functional.

Step 9- Combine Splash Screen and Main Interface

In this final step, we integrate the Splash Screen and the Main Interface (WeatherHunt) into the app's entry point (App.js). This setup ensures that the Splash Screen appears for a few seconds before transitioning to the main weather application screen. The transition is managed using a simple state (showSplash) and the useEffect hook to set a timer.

Here is the full App.js code:

import React, { useState, useEffect } from 'react';
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import WeatherHunt from './screens/WeatherHunt';
import SplashScreen from './splashscreen/SplashScreen';

export default function App() {
  const [showSplash, setShowSplash] = useState(true);

  useEffect(() => {
    const timer = setTimeout(() => {
      setShowSplash(false);
    }, 4000); // 4 seconds

    return () => clearTimeout(timer); // Cleanup the timer
  }, []);

  return (
    <>
      <StatusBar style="auto" />
      {showSplash ? <SplashScreen /> : <WeatherHunt />}
    </>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

Enter fullscreen mode Exit fullscreen mode

Here is the Live Demo.
weather app by react native using opencageapi and openweather map by robinokwanma 1

weather app by react native using opencageapi and openweather map by robinokwanma 2

weather app by react native using opencageapi and openweather map by robinokwanma 3

weather app by react native using opencageapi and openweather map by robinokwanma 4

Conclusion

Congratulations! 🎉 You’ve successfully built a functional and visually appealing live weather application using React Native, Expo, OpenWeatherMap and OpenCageData API. This app includes a splash screen, a searchable interface, and dynamic weather data fetched from an API.

The complete source code for this project is available here. Feel free to explore and modify it as needed.

If you have any questions or face any challenges while building this project, leave a comment below, and I’ll be happy to assist. Happy coding! 🚀

Top comments (0)