React Weather App: how it's made! [Episode 1]
Fetching API data, setting up Redux, fun with hooks
I've been posting lately on professional social media about this weather app I've been working on. I felt very "limited" by the platform though, because I wanted to be able to share code and explain it, describe the architecture and so on, so here I am!
Anyways, let's look at the app!
Pretty cool hey? :)
It provides worldwide weather forecast , 5 days ahead, with 3 hours periods. It features some cool charts with loads of fancy numbers, pretty icons and even flags! Wanna know what time the sun sets? you can!
you can see the app in action here: clementbenezech.github.io/react-meteo-app
Note that it can also be used on mobile with a mobile oriented layout.
So, "how did he manage to create such a decent looking mildly exciting single page app?", you might be asking yourself. Alright then. I'll tell you all my dirty little secrets.
Note: I'll tell you how I made it. It doesn't mean it was the best / easiest / cleanest way to do it. The only intent is to share the general process I followed, and at the same time try to illustrate how you can make things interact with each other in react. Do feel free to share your thoughts on the technical relevance of my choices :)
For everything else, there is that: reactjs.org/docs/getting-started.html :p
It started a few weeks ago, I had just finished another side project and wanted to start a new one including some API calls. Nothing Fancy. So I went for the weather option because I came across this API providing free weather forecasts API's:
Don't know what an API is? It stands for Application Programming Interface, it consist of an URL you can query with a specific syntax, which will send you back some data (in this case) or even perform update, delete, or create operations. It basically is a way for the front end layer to interact with the back-end.
Perfect start. Gives you data like this:
So I went with it and created the first version of the app, a very simple one, like this:
Don't know how to create a react-app? Go here: create-react-app.dev/docs/getting-started
- First, I created a simple layout for the page using SCSS for styling and JSX for content. For this project I went desktop first. Just simple cards and containers displaying only raw data.
Want to add sass to react? create-react-app.dev/docs/adding-a-sass-sty..
- Then I created an account on openweathermap.org to get an API access token, and started writing my first API call, and soon I had something working, I was fed data! This is how you do it:
export const OWCityGetData = (city, apiType) => {
fetch("https://api.openweathermap.org/data/2.5/"+apiType+"?q="+city+"&units=metric&appid={your_key}")
.then (response => response.json())
.then (response => {
console.log(response)
const data = response;
if (data.cod != "404") {
if (apiType == "weather") {
/*do something*/
}
else if (apiType == "forecast") {
/*do something*/
}
}
else {
if (apiType == "weather") {
/*do something*/
}
else if (apiType == "forecast") {
/*do something*/
}
}
})
}
Then... I started interrogating myself... At first, I thought that components should make API calls, but then I realized all components were actually using the same data, so why multiply API calls, when there was only going to be one input on the page, the city name.
So each time the city name was changed, components would have to render to match new query.
So the first thing I needed to do was to make the data available to all top level components. And since I actually used Redux for another project just before that, I went for it. It might seem a bit too much, however , we'll have other use for that later, and it is actually pretty simple to set up.
To add redux to the project, you just need to do:
# If you use npm:
npm install react-redux
# Or if you use Yarn:
yarn add react-redux
Then you need to create two files:
-AppReducer.js
const initialState = {currentWeather: null,
forecast :null,
city : "toulouse",
currentDay : null,
currentTime: null,
timezone: null }
// Use the initialState as a default value
export default function AppReducer(state = initialState, action) {
// The reducer normally looks at the action type field to decide what happens
switch (action.type) {
case 'api/doSomething' : {
return {
...state,
something : action.something
}
}
default:
// If this reducer doesn't recognize the action type, or doesn't
// care about this specific action, return the existing state unchanged
return state
}}
- Store.js
import { createStore } from 'redux'
import AppReducer from "./AppReducer"
//Creating the store
let store = createStore(AppReducer);
//checking initial value
console.log('Initial state: ', store.getState());
export default store
The store is where the data is going to be... well... stored :) Then any component who subscribed to the store can access the data and REACT when the data changes!
The Reducer is where we define the actions taken depending on which "route" of the reducer was used. For us, this will revolve around updating the value of some key parameters:
- Current city
- Current city timezone, needed to determine local time.
- Current Day: The day being selected by the user. This will be updated by a menu allowing to select a specific day in the five days forecast.
- Current Time: The time of day selected, used to see detailed forecast of a specific timeframe.
- Forecast: the forecast data for the current city.
We also need to alter index.js to add a provider to . This is how we make the store that we created available to all components of the app.
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './components/App'
import reportWebVitals from './reportWebVitals'
import { Provider } from 'react-redux'
import store from "./components/Store.js"
ReactDOM.render(
<React.StrictMode>
<Provider store={store} >
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
Now, whenever we update one value, some components will become aware the state has changed. Those components will render again, update themselves, and may also update the Global State, making other components update and so on and so on.
In our case, it goes like this:
The user inputs a city into the search field.
On submit, the component pushes the city name in the state.
The App (root) component is advised there has been a change in the value of state.city, and renders again.
It gathers both forecast and current weather data for the current city, and updates the value of forecast in the state.
Root forecast component becomes aware that state.forecast has changed, and it renders again. CurrentWeather component does the same.
DaySelector component(the menu to select a specific day) analyses dates in the forecast data and build itself.
The user selects a day, Dayselector component updates the value of currentDay in the redux state, and the forecast component render forecast cards for the seleted day.
The user selects a "3 hours weather card", the forecastCard updates the value of currentTime in the redux state, then DetailedForecast renders himself with the new data.
For each process, we need to create an action in the reducer which will update the state the way we want.
Now, let's say we have those actions defined:
export default function AppReducer(state = initialState, action) {
// The reducer normally looks at the action type field to decide what happens
switch (action.type) {
case 'api/putWeatherInState' : {
return {
...state,
currentWeather : action.payload
}
}
case 'api/putForeCastInState' : {
return {
...state,
forecast : action.payload
}
}
case 'api/putCityInState' : {
return {
...state,
city : action.payload
}
}
case 'api/putTimezoneInState' : {
return {
...state,
timezone : action.payload
}
}
case 'api/putCurrentDayInState' : {
return {
...state,
currentDay : action.payload
}
}
case 'api/putCurrentTimeInState' : {
return {
...state,
currentTime : action.payload
}
}
And we do this in a component:
import { useDispatch } from "react-redux";
const dispatch = useDispatch()
dispatch({ type: 'api/putCityInState', payload: city})
Then we are actually dispatching an action to the reducer, using the useDispatch Hook. This action is an object with two properties:
- action.type: The "Route" of the action.
- action.payload: the value to be processed by the action.
So what will happen here is this:
case 'api/putCityInState' : {
return {
...state,
city : action.payload
}
}
The value of action.type matches, so the state will be updated with the new city value contained in action.payload.
And this is pretty much how it is done. So in every component, just dispatch whatever you want to the state, whenever you choose. Of course those are very simple actions, but you can also do more complex stuff in there. Below is an example from another of my projects, just to illustrate, it does not relate with the weather app in any way:
case 'product/addToCart': {
/*Handling the addProduct case:
If the product already exists in the state, then increment its quantity by specified quantity
Else, the product is added to the state with the specified quantity*/
if (state.cartProducts.findIndex(currentProduct => currentProduct.id === action.payload.id) != -1) {
return {
...state,
cartProducts : state.cartProducts.map(cartProduct => cartProduct.id === action.payload.id ? {
...cartProduct, quantity: cartProduct.quantity + action.payload.quantity
} : cartProduct
)
}
}
else {
return {
...state,
cartProducts: [...state.cartProducts, action.payload]
}
}
}
Remember the OWCityGetData function we wrote before? The one fetching data from openweathermap API?
It takes two arguments:
- The name of the city to query.
- The type of data we wish to retrieve: either current weather data or forecast data.
It checks if we don't get a 404 error code in the response, then it inserts either the data or an error code in the right store property
So now, we need to add this behavior in the function using useDispatch Hook:
import { useDispatch } from "react-redux";
export const OWCityGetData = (city, apiType) => {
const dispatch = useDispatch();
fetch("https://api.openweathermap.org/data/2.5/"+apiType+"?q="+city+"&units=metric&appid={APIKEY}")
.then (response => response.json())
.then (response => {
console.log(response)
if (data.cod != "404") {
if (apiType == "weather") {
dispatch({ type: 'api/putWeatherInState', payload: data})
dispatch({ type: 'api/putTimezoneInState', payload: data.timezone})
}
else if (apiType == "forecast") {
dispatch({ type: 'api/putForeCastInState', payload: data})
dispatch({ type: 'api/putTimezoneInState', payload: data.city.timezone})
}
}
else {
if (apiType == "weather") {
dispatch({ type: 'api/putWeatherInState', payload: "error"})
}
else if (apiType == "forecast") {
dispatch({ type: 'api/putForeCastInState', payload: null})
}
}
})
}
Now, any time we call this function (OWCityGetData), it will fetch the data we want and put it in the store. This works because we defined actions to handle those changes, in the reducer.
I feel like I must have you fed up by now, but there is one other thing we need to be able to do, because there is no use for us to be updating values in the store if we cannot access it! But the good news is, we can! We just have to use another hook:
For example, if we want to subscribe to the value of state.forecast, we do this in our component:
import { useSelector } from "react-redux";
/*Inside the component*/
const forecastWeatherData = state => state.forecast;
const cityForecastWeatherData = useSelector(forecastWeatherData);
And that is all!
Now, we can use the hook we defined (here, it is cityForecastWeatherData) wherever we want in the component (well almost wherever we want cause there are rules of hooks: ru.react.js.org/docs/hooks-rules.html).
Whenever the state is updated (meaning useDispatch is used somewhere), the component render and updates itself. Simple as that.
Now, we know how to make use of Redux, but just to be sure, let's summarize:
- Create a store.
- Provide the store to the app using a provider.
- Create a Reducer.
- Define actions in the reducer.
Then make use of the reducer by using useDispatch, and subscribe to state values with useSelector in components to have them update themselves whenever the state is altered.
I hope you enjoyed the ride. Next time I'll show you how we actually do all that and how the component themselves are written. But we've got good foundations!
See you soon for more, thanks for reading.