Weather app in React, Redux, Typescript, and Tailwind
Tags: react, redux, typescript, tailwindcss
Published: 2020-08-02 18:20:01

Hello reader 👋👋,



In this article, you will learn how to make basic weather app in React, Redux, and Typescript.

The React part is written in Typescript.

The Redux part is written in plain Javascript for the sake of **simplicity**.

This article is meant for beginners in React. I have about a year's experience in Vue and Vuex before I started learning React and Redux. It's **best suited** for those readers.

**[Let me show you the app](https://rrtt-weather.netlify.app)**, then we will mix reverse engineering and going from step one to the final app.

Hope you liked it! If you don't have time to read, here is the [source code](https://github.com/kenosej/weather-react). 👈



## Let's start

Requirements: [**node.js** and **npm**](https://nodejs.org/en/download/).

****

## Step 1

#### Install packages



Let's first execute commands, then I will explain what each command does.

Open your terminal and execute commands:


```
npx create-react-app weather --template typescript
```

```
cd weather
```

```
npm install react-icons react-redux react-router-dom redux redux-thunk tailwindcss postcss-cli autoprefixer @fullhuman/postcss-purgecss @types/react-redux @types/react-router-dom
```



Take a look at [why React does not put dependencies in devDependendencies](https://github.com/facebook/create-react-app/issues/4342).

The **first** command builds **React template in Typescript**. We named our app "weather".

The **second** command moves us into the application directory.

The **third** command installs packages:

​	[`react-icons`](https://react-icons.github.io/react-icons/) - for fancy icons

​	[`react-redux`](https://react-redux.js.org/) - for connecting Redux with React

​	[`react-router-dom`](https://www.npmjs.com/package/react-router-dom) - for enabling many routes and SPA navigation (SPA - Single Page Application)

​	[`redux`](https://redux.js.org/) - for state management

​	[`redux-thunk`](https://github.com/reduxjs/redux-thunk) - for enabling asynchronous behavior in redux

​	[`tailwindcss`](https://tailwindcss.com/) - CSS framework for easier styling

​	[`postcss-cli`](https://github.com/postcss/postcss-cli) - for enabling minifying app for production (CSS file gzipped from ~140kb to ~3kb... WORTH IT 🧐)

​	[`autoprefixer`](https://github.com/postcss/autoprefixer) - for parsing CSS and adding vendor prefixes to CSS rules

​	[`@fullhuman/postcss-purgecss`](https://www.npmjs.com/package/@fullhuman/postcss-purgecss) - PostCSS plugin for PurgeCSS

​	[`@types/react-redux`](https://www.npmjs.com/package/@types/react-redux) - type definitions for react-redux (for Typescript)

​	[`@types/react-router-dom`](https://www.npmjs.com/package/@types/react-router-dom) - type definitions for React Router (for Typescript)


Let's start application:

```
npm start
```

****

## Step 2

#### Remove auto-generated code



Let's remove minimal code that interferes with our goals, for now.

Go into ./src/App.tsx and remove code inside return statement to look like:

```tsx
return <></>;
```

At the top you can see: 

```tsx
import logo from "./logo.svg";
import "./App.css";
```

Remove both imports and **delete** ./src/App.css.

If you see a white screen on your browser, you are good to go.

For now, it's good. Delete other useless code if you want, but to keep this post shorter, I will cut it here.

****

## Step 3

#### Building structure



We need to make five new directories inside ./src.

Inside ./src make:

- actions

- assets

- components

- pages

- reducers

  

**Explanation**:

- actions - for storing redux actions and action types
- assets - for static content, like images
- components - it's always a good thing to strive for the Single Responsibility Principle. In a bigger project, you will be able to use the same component multiple times and save time for everyone
- pages - a place of clean code and separate concerns where you connect routes to components
- reducers - place where dispatched redux actions changes the state of the application

****

## Step 4

#### Enable Tailwind



Let's add Tailwind to the application.

Open `./src/index.tsx` and add:

```javascript
import "./tailwind.output.css";
```

Also, add *./tailwind.config.js*, so we learn how to add custom properties to Tailwind.

*./tailwind.config.js*

```js
module.exports = {
  theme: {
    extend: {
      width: {
        "410px": "410px",
      },
    },
  },
};
```

Before `npm start` and `npm run build` we want to build Tailwind also.

To solve this problem, in "scripts" tag in *package.json* add:

```
"build:tailwind": "tailwind build src/tailwind.css -o src/tailwind.output.css",
"prestart": "npm run build:tailwind",
"prebuild": "npm run build:tailwind"
```

Adding "**pre**" before the start and build, will run the desired command before **every** `npm start` and `npm run build`.

As you can see, there is src/tailwind.css, which is not created yet. So, let's do it.

*./src/tailwind.css*:

```
@tailwind base;
@tailwind components;
@tailwind utilities;
```

Stop watching changes in code by npm by hitting Ctrl + C on Windows in the terminal.

Again, run `npm start` to compile code. You should now see something like in console:

*npm run build:tailwind*

And *tailwind.output.css* should appear in ./src.

****

## Step 5

#### Prepare Redux



In *./src/reducers* make:

*./src/reducers/ajaxReducer.js*:

```javascript
const initialState = {
    weather: {},
  };

  export default function (state = initialState, action) {
      switch (action.type) {
          default:
              return state;
      }
  }
```

We will fetch data from [OpenWeatherMap](https://openweathermap.org/), so we need a place to store data.

Data will be store in the weather, in the state.

For now, let's write the boilerplate code.

*./src/reducers/index.js*:

```javascript
import { combineReducers } from "redux";
import ajaxReducer from "./ajaxReducer";

export default combineReducers({
  weatherReducer: ajaxReducer,
});
```

At *index.js* combine all the reducers. We have only one - ajaxReducer in this project, but it won't be always the case.

At a **large** project, having index.js - a central place of Redux reducers is a good thing, "clean code".

Time for **action**.. actions!

Let's make *types.js* where we store all types of Redux actions. It's like *./src/reducers/index.js* for actions.

In this simple project, we will only have one action.

*./src/actions/types.js*

```javascript
export const FETCH_WEATHER = "FETCH_WEATHER";
```

And, let's make one and only ajax request/redux action. Before that, you need to go to the [OpenWeatherMap](https://openweathermap.org/) and make a token.

A token is a requirement for using [OpenWeatherMap](https://openweathermap.org/), that is generous enough to give us a very high number of API calls for free.

*./src/actions/ajaxActions.js*

```javascript
import { FETCH_WEATHER } from "./types";

export const fetchWeather = () => async (dispatch) => {
  const ids = {
    Munich: 2867714,
    London: 2643743,
    California: 4350049,
  };

  const fetches = await Promise.all(
    Object.values(ids).map((e) =>
      fetch(
        `https://api.openweathermap.org/data/2.5/forecast?id=${e}&appid=` // here you put your token key
      ).then((e) => e.json())
    )
  );

  dispatch({
    type: FETCH_WEATHER,
    payload: {
      // iterating through object does not guarantee order, so I chose manually
      Munich: fetches[0],
      London: fetches[1],
      California: fetches[2],
    },
  });
};
```

I chose those cities because I like them. You can pick the cities which you like. [Here](http://bulk.openweathermap.org/sample/city.list.json.gz) you can find IDs.

Explanation of *./src/actions/ajaxActions.js*:

1. Import type, so we can connect type with defined action
2. Make an object of city names and IDs
3. Store fetched and parsed JSON into constant *fetches*. Use **Promise.all()** for fetching data of cities concurrently. URL needs city ID and also **Promise.all()** expects argument of an array type. Do it by making an array from the object of cities and their ID with **Object.values()**. Iterate through it with a high-order function **map**, which returns the array. **Fetch** does not parse JSON, and it's asynchronous, so first wait for fetching data. Then "unpack" (parse) it by another asynchronous method: **JSON**. You could use **await** keyword again, but I prefer **then**, it seems like beautiful syntax.
4. In the argument, you can see that we grabbed dispatch, so we can later dispatch an action to the store. If it isn't understandable, read about Javascript **closures**.
5. In the end, we call dispatch and pass an object with two keys: type and payload. In type, we link type from *./src/actions/types.js*, and in payload, we store data returned from API. There are a lot of ways to not duplicate yourself in this code, but I chose this way for **simplicity's** sake.

We left ajaxReducer.js unfinished. It's time to complete it.

*./src/reducers/ajaxReducer.js*

```js
import { FETCH_WEATHER } from "../actions/types";

const initialState = {
  weather: {},
};

export default function (state = initialState, action) {
  switch (action.type) {
    case FETCH_WEATHER:
      return {
        ...state,
        weather: action.payload,
      };
    default:
      return state;
  }
}
```

As you can see, Redux does not allow us to change just one bit of a state from reducers. First, destructure the current state. Immediately after, overwrite weather key with action payload from *./src/actions/ajaxActions.js*. 

****

## Step 6

#### Connect app to redux



Let's first make the main file of Redux. If you worked before with Vuex, you will recognize a pattern here. Vuex and Redux are very similar.

Both have the same purpose, but Vuex is a little easier to understand. Let's name the main Redux file.

*./src/store.js*

```js
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import rootReducer from "./reducers";

const initialState = {};

const middleware = [thunk];

const store = createStore(
  rootReducer,
  initialState,
  applyMiddleware(...middleware)
);

export default store;
```

Make it super clean. The code is self-explaining. Clean boilerplate for bigger projects.



In *./src/App.tsx* it's time to make some changes.

*./src/App.tsx*

```tsx
import React from "react";
import { Provider } from "react-redux";
import store from "./store";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";

import Home from "./pages/Home";

function App() {
  return (
    <Provider store={store}>
      <Router>
        <Switch>
          <Route path="/" component={Home} />
        </Switch>
      </Router>
    </Provider>
  );
}

export default App;
```

To make React application work with Redux, we need to wrap the app in <Provider>, which receives *./src/store.js*. It's possible to have multiple stores. Saw it before, but not a big fan //yet 🤣.

You saw a couple of errors in your terminal if you saved your code, I'm sure. It's time to make a first page - Home.

****

## Step 7

#### Naming assets



For background of cards on home page, I use gifs, so here are names (put whatever gifs you like):


*./src/assets/clear.gif*

*./src/assets/clouds.gif*

*./src/assets/drizzle.gif*

*./src/assets/fog.gif*

*./src/assets/rain.gif*

*./src/assets/snow.gif*

*./src/assets/thunderstorm.gif*



For Home page eight pictures are used. Four for phones, four for desktops.

**For phones**:

​	*./src/assets/p_bg1.jpg*

​	...

​	*./src/assets/p_bg4.jpg*



**For desktops**:

​	*./src/assets/d_bg1.jpg*

​	...

​	*./src/assets/d_bg4.jpg*

****

## Step 8

#### Home and its components



`./src/pages/Home.tsx`

```tsx
import React, { Component } from "react";

import Card from "../components/home/Card";
import { connect } from "react-redux";
import { fetchWeather } from "../actions/ajaxActions";

interface FormProps {
  fetchWeather: Function;
  weather: Record<string, any>;
}

interface FormState {
  random: number;
  imageSource: string;
}

class Home extends Component<FormProps, FormState> {
  constructor(props: any) {
    super(props);

    const randomInt = (min: number, max: number) =>
      Math.floor(Math.random() * (max - min)) + min; // generate random integer

    this.state = {
      random: randomInt(1, 5), // randomly select background, whose names ends with 1 | 2 | 3 | 4
      imageSource: "",
    };
  }

  // select randomly/change background on click
  setBg = (type: "default" | "click"): void => {
    if (type === "default") {
      this.setState({
        imageSource: require(`../assets/${
          window.innerWidth < 768 ? "p" : "d"
        }_bg${this.state.random}.jpg`),
      });
    } else if (type === "click") {
      // increase random num, then call recursive callback
      if (this.state.random === 4) {
        return this.setState(
          {
            random: 1,
          },
          () => this.setBg("default")
        );
      }

      return this.setState(
        {
          random: this.state.random + 1,
        },
        () => this.setBg("default")
      );
    }
  };

  componentDidMount() {
    this.props.fetchWeather();
    this.setBg("default");
    window.addEventListener("resize", () => this.setBg("default"));
  }

  render() {
    return (
      <div
        className="h-screen w-screen bg-cover bg-center"
        style={{
          backgroundImage: `url(${this.state.imageSource})`,
        }}
        onClick={() => this.setBg("click")}
      >
        <div
          className="flex flex-col justify-center items-center w-screen"
          style={{ height: "95%" }}
        >
          {Object.keys(this.props.weather).map((e, i) => {
            return <Card city={e} key={i} weather={this.props.weather[e]} />;
          })}
        </div>
      </div>
    );
  }
}

const mstp = (state: { weatherReducer: { weather: {} } }) => ({
  weather: state.weatherReducer.weather,
});

export default connect(mstp, { fetchWeather })(Home);
```

Take **advantage** of Typescript, by predefining types of component props and state.

Define the component as a **class** component. The same thing can be done with **React Hooks**.

The thing to remember at expression setBg is that setState **won't** immediately set state, so take advantage of its second argument. It receives callback which will execute **immediately** after the state is updated. And then its time for the recursive call, to change background photo.

- The single argument of an arrow function you could write without parentheses. For clarity purposes, let's keep 'em

*./src/components/home/Card.tsx*

**Name your components with a capital letter!**

```tsx
import LeftComponent from "./LeftComponent";
import { Link } from "react-router-dom";
import React from "react";
import { RiMapPinLine } from "react-icons/ri";
import RightComponent from "./RightComponent";
import Tomorrow from "./Tomorrow";
import { determineGif } from "../Utils";

interface FormProps {
  city: string;
  weather: any;
}

function Card(props: FormProps) {
  // find min. and max. temperatures from all timestamps from today
  const findMinAndMaxTemps = (list: any[]): [number, number] => {
    const d = new Date();

    const today = d.getDate() < 10 ? `0${d.getDate()}` : d.getDate();
    let min: number[] = [],
      max: number[] = [];

    list.forEach((e) => {
      if (`${e.dt_txt[8]}${e.dt_txt[9]}` === today.toString()) {
        min.push(e.main.temp_min);
        max.push(e.main.temp_max);
      }
    });

    return [
      Math.round(Math.min(...min) - 273.15),
      Math.round(Math.max(...max) - 273.15),
    ];
  };

  let temperature = 0,
    minTemperature = 0,
    maxTemperature = 0,
    stateOfWeather = "",
    feelsLike = 0,
    speed = 0,
    deg = 0,
    idOfWeather = 0,
    day = true,
    list = [];

  if (props.weather?.list) {
    temperature = Math.round(props.weather.list[0].main.temp - 273.15);
    [minTemperature, maxTemperature] = findMinAndMaxTemps(props.weather.list);
    stateOfWeather = props.weather.list[0].weather[0].main;
    feelsLike = Math.round(props.weather.list[0].main.temp - 273.15);
    speed = props.weather.list[0].wind.speed;
    deg = props.weather.list[0].wind.deg;
    idOfWeather = props.weather.list[0].weather[0].id;
    day = props.weather.list[0].sys.pod === "d";
    list = props.weather.list;
  }

  const [classes, url] = determineGif(idOfWeather);

  return (
    <Link to={`/${props.city}`} className="h-40 w-full sm:w-410px">
      <div className="flex h-40 w-full sm:w-410px">
        <div
          className={`text-white m-2 rounded-lg flex-grow bg-left-bottom ${classes}`}
          style={{
            backgroundImage: `url(${url})`,
          }}
        >
          <div className="flex w-full h-full divide-x divide-gray-400 ">
            <div className="w-9/12">
              <div
                className="mt-2 ml-2 p-2 rounded-lg inline-block text-xs"
                style={{
                  boxShadow: "0 0 15px 1px rgba(0, 0, 0, 0.75)",
                  backdropFilter: "blur(2px)",
                }}
              >
                <div className="flex items-center">
                  <RiMapPinLine />
                  <div className="ml-2">{props.city}</div>
                </div>
              </div>
              <div className="w-full flex justify-around items-center">
                <LeftComponent
                  stateOfWeather={stateOfWeather}
                  idOfWeather={idOfWeather}
                  day={day}
                />
                <div className="flex flex-col text-center">
                  <div className="text-5xl">{temperature}°</div>
                  <div className="text-lg">
                    {minTemperature}/{maxTemperature}°
                  </div>
                </div>
                <RightComponent speed={speed} deg={deg} feelsLike={feelsLike} />
              </div>
            </div>
            <Tomorrow idOfWeather={idOfWeather} day={day} list={list} />
          </div>
        </div>
      </div>
    </Link>
  );
}

export default Card;
```

If you are curious about *determineGif*, continue reading, we are almost there!

Take a look at an [API response structure](https://openweathermap.org/api/hourly-forecast), so you can understand variable pairing.

The API response is in Kelvin, so to get Celsius you need to subtract 273.15.

You could do the same thing by passing units=metric at request URL, but its great to meet Javascript floating point number precision.

Remove **Math.round()** and time will tell you about it 🤣.

As you can see, we get into Tailwind. Tailwind is nice, I would say 'micro' CSS framework, that almost doesn't let you write raw CSS. I don't like it like I do Vuetify, but if you need to manage style at a low and small level, it's great! The thing that I most like about it, it's great [documentation](https://tailwindcss.com/).

This component could be separated into smaller parts. But to be time-friendly, I kept it relatively "big".

There are 3 more components, so let's explore 🧐.

*./src/components/home/LeftComponent.tsx*

```tsx
import React from "react";
import { determineIcon } from "../Utils";

interface FormProps {
  stateOfWeather: string;
  idOfWeather: number;
  day: boolean;
}

function LeftComponent(props: FormProps) {
  return (
    <div className="flex flex-col text-center">
      {determineIcon(props.idOfWeather, props.day, "h-16 w-16")}
      <div>{props.stateOfWeather}</div>
    </div>
  );
}

export default LeftComponent;
```

*./src/components/home/RightComponent.tsx*

```tsx
import React from "react";

interface FormProps {
  feelsLike: number;
  deg: number;
  speed: number;
}

function RightComponent(props: FormProps) {
  const determineLevel = (temp: number): string[] => {
    if (temp < 10 || temp > 29) {
      return ["Bad", "bg-red-600"];
    }

    if ((temp > 9 && temp < 18) || (temp > 22 && temp < 30)) {
      return ["ok", "bg-yellow-600"];
    }

    if (temp > 17 && temp < 23) {
      return ["Good", "bg-green-600"];
    }

    return [];
  };

  const determineSide = (deg: number): string | undefined => {
    if (deg < 30) return "N";

    if (deg < 60) return "NE";

    if (deg < 120) return "E";

    if (deg < 150) return "ES";

    if (deg < 210) return "S";

    if (deg < 240) return "SW";

    if (deg < 300) return "W";

    if (deg < 330) return "NW";

    if (deg < 360) return "N";
  };

  const feelsLikeProperties = determineLevel(props.feelsLike);

  return (
    <div className="self-end text-center">
      <div
        className={`${feelsLikeProperties[1]} rounded-lg text-xs sm:text-sm p-1`}
      >
        {props.feelsLike} {feelsLikeProperties[0]}
      </div>
      <div className="mt-1 text-xs md:text-sm">
        {determineSide(props.deg)} {Math.round(props.speed * 3.6)} km/h
      </div>
    </div>
  );
}

export default RightComponent;
```

*determineLevel* return could be better, but let's keep it **simple**.

Wind response is in *m/s*, so to convert it to *km/h* multiply by 3.6.

*determineSide* is there for determining if its north, east...

I have **a challenge** for you - after you make this application, try to make a feature to toggle wind speed between *m/s*, *km/h*, and *km/s*.

*./src/components/home/Tomorrow.tsx*

```tsx
import React from "react";
import { RiArrowRightSLine } from "react-icons/ri";
import { determineIcon } from "../Utils";

interface FormProps {
  idOfWeather: number;
  day: boolean;
  list: [];
}

function Tomorrow(props: FormProps) {
  const determineNextDayAbb = (): string => {
    const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

    let date = new Date();
    let index: number;

    if (date.getDay() === 6) {
      index = 0;
    } else {
      index = date.getDay() + 1;
    }

    return weekdays[index];
  };

  const crawlNextDayTemps = (list: any[]): [number, number] | void => {
    const d = new Date();
    d.setDate(d.getDate() + 1); // tomorrow

    const tomorrow = d.getDate() < 10 ? `0${d.getDate()}` : d.getDate();

    let min: number[] = [],
      max: number[] = [];

    list.forEach((e) => {
      if (`${e["dt_txt"][8]}${e["dt_txt"][9]}` === tomorrow.toString()) {
        min.push(e.main.temp_min);
        max.push(e.main.temp_max);
      }
    });

    return [
      Math.round(Math.min(...min) - 273.15),
      Math.round(Math.max(...max) - 273.15),
    ];
  };

  const nextDayTemps = crawlNextDayTemps(props.list);

  return (
    <div className="w-3/12">
      <div className="flex justify-between p-2">
        <div className="text-xs">{determineNextDayAbb()}</div>
        <div className="text-xs flex items-center">
          <div>More</div>
          <RiArrowRightSLine />
        </div>
      </div>
      <div className="flex flex-col text-center">
        <div className="w-full">
          {determineIcon(props.idOfWeather, props.day, "h-16 w-16 mx-auto")}
        </div>
        <div className="text-lg">
          {Array.isArray(nextDayTemps) ? nextDayTemps[0] : "?"}/
          {Array.isArray(nextDayTemps) ? nextDayTemps[1] : "?"}°
        </div>
      </div>
    </div>
  );
}

export default Tomorrow;
```

Expression names are self-explaining. The classical example of a functional component.

****

## Step 9

#### City and its components



It's a pretty long article. A lot longer than I expected to 😅. 
Let's first add the city route to React. 

*./src/App.tsx*

**Before**

```tsx
<Route path="/" component={Home} />
```

add:

```tsx
<Route path="/:city" component={City} />
```

Add the "City" route before the "Home" route, or take advantage of `exact` prop.

At the top of *./src/App.tsx* add:

```javascript
import City from "./pages/City";
```



*./src/pages/City.tsx*

```tsx
import React, { Component } from "react";

import Desktop from "../components/city/Desktop";
import { connect } from "react-redux";
import { fetchWeather } from "../actions/ajaxActions";

// match.params.city is URL (react-router) variable
interface FormProps {
  fetchWeather: Function;
  match: {
    params: {
      city: string;
    };
  };
  weather: Record<string, any>;
}

interface FormState {
  imageSrc: string;
  random: number;
}

class City extends Component<FormProps, FormState> {
  constructor(props: any) {
    super(props);

    if (
      this.props.match.params.city !== "Munich" &&
      this.props.match.params.city !== "London" &&
      this.props.match.params.city !== "California"
    ) {
      window.location.replace("/404");
      return;
    }

    if (!Object.keys(this.props.weather).length) {
      // fetch from api, if city is accessed directly
      this.props.fetchWeather();
    }

    const randomInt = (min: number, max: number) =>
      Math.floor(Math.random() * (max - min)) + min;

    this.state = {
      imageSrc: "",
      random: randomInt(1, 3), // choose random photo from 2 available photos
    };
  }

  updateDimensions = () => {
    // change background photo for phone/desktop
    this.setState({
      imageSrc: require(`../assets/${
        window.innerWidth < 768 ? "p" : "d"
      }_${this.props.match.params.city.toLowerCase()}${this.state.random}.jpg`),
    });
  };

  componentDidMount() {
    this.updateDimensions();
    window.addEventListener("resize", this.updateDimensions);
  }

  render() {
    return (
      <div
        className="h-screen w-screen bg-cover bg-center"
        style={{
          backgroundImage: `url(${this.state.imageSrc})`,
        }}
      >
        <Desktop
          city={this.props.match.params.city}
          info={this.props.weather[this.props.match.params.city]}
        />
      </div>
    );
  }
}

const mstp = (state: { weatherReducer: { weather: {} } }) => ({
  weather: state.weatherReducer.weather,
});

export default connect(mstp, { fetchWeather })(City);
```

As you can see, if the URL is not these 3 cities, we redirect the user to the 404 pages. **Challenge** here for you is to make a good-looking 404 page.

The same pattern for changing background photo is used here.

In case the user enters URL directly, the application fetches data from API if there's no data in the state.


Here is the elephant of the code 😅

*./src/components/city/Desktop.tsx*

```tsx
import React, { useState } from "react";
import { WiHumidity, WiStrongWind } from "react-icons/wi";

import { GiCrossedAirFlows } from "react-icons/gi";
import { MdVisibility } from "react-icons/md";
import { determineIcon } from "../Utils";

interface FormProps {
  city: string;
  info: any;
}

function Desktop(props: FormProps) {
  const [day, setDay] = useState(0);
  const [hour, setHour] = useState(0);

  const blurredChip = {
    boxShadow: "0 3px 5px rgba(0, 0, 0, 0.3)",
    backdropFilter: "blur(2px)",
  };

  const determineNext5Days = (): string[] => {
    const days = [
      "Sunday",
      "Monday",
      "Tuesday",
      "Wednesday",
      "Thursday",
      "Friday",
      "Saturday",
    ];

    let next5Days = [];

    for (let i = 0; i < 4; i++) {
      const d = new Date();
      d.setDate(d.getDate() + i);

      next5Days.push(days[d.getDay()]);
    }

    return next5Days;
  };

  interface Simplified {
    time: string;
    temp: number;
    feelsLike: number;
    weatherID: number;
    weatherState: string;
    day: boolean;
    humidity: number;
    pressure: number;
    windSpeed: number;
    visibility: number;
  }

  // pluck relevant info of todays timestamps
  const determineTimestamps = (day: number, list: any[]): any[] => {
    const d = new Date();
    d.setDate(d.getDate() + day);

    const timestamps: Simplified[] = [];

    for (const e of list) {
      if (parseInt(`${e["dt_txt"][8]}${e["dt_txt"][9]}`) === d.getDate()) {
        timestamps.push({
          time: e.dt_txt.slice(11, 16),
          temp: Math.round(e.main.temp - 273.15),
          feelsLike: Math.round(e.main.feels_like - 273.15),
          weatherID: e.weather[0].id,
          weatherState: e.weather[0].main,
          day: e.sys.pod === "d",
          humidity: e.main.humidity,
          pressure: e.main.pressure,
          windSpeed: Math.round(e.wind.speed * 3.6),
          visibility: Math.round(e.visibility / 100),
        });
      }
    }

    return timestamps;
  };

  // rather return the last timestamps than earlier ones (e.g. 21:00 > 03:00)
  const checkTerrain = (squares: number, tss: Simplified[]) => {
    let cut: any[] = [];

    const numberOfNeededRemoval = tss.length - squares;

    if (numberOfNeededRemoval < 0) return tss;

    for (let i = numberOfNeededRemoval; i < tss.length; i++) {
      cut.push(tss[i]);
    }

    return cut;
  };

  const adaptToWidth = (tss: Simplified[]) => {
    // show minimum four squares of timestamps to max 8
    if (tss.length < 5) return tss;

    if (window.innerWidth < 950) {
      return checkTerrain(4, tss);
    } else if (window.innerWidth < 1150) {
      return checkTerrain(5, tss);
    } else if (window.innerWidth < 1250) {
      return checkTerrain(6, tss);
    } else if (window.innerWidth < 1350) {
      return checkTerrain(7, tss);
    }

    return checkTerrain(8, tss);
  };

  // until info from api is fetched
  const timestamps = props.info?.list
    ? adaptToWidth(determineTimestamps(day, props.info?.list))
    : [];

  if (!timestamps.length) {
    return <></>;
  }

  // after fetch
  return (
    <>
      <div className="w-screen flex justify-between" style={{ height: "65%" }}>
        <div className="text-white pt-8 pl-8">
          <div className="text-6xl">
            {determineIcon(timestamps[hour].weatherID, timestamps[hour].day)}
          </div>
          <div className="text-4xl my-1 sm:my-0">
            {timestamps[hour].weatherState}
          </div>
          <div className="text-xl my-1 sm:my-0">{props.city}</div>
          <div className="text-5xl font-bold">{timestamps[hour].temp}°C</div>
        </div>
        <div className="mt-20 mr-4 md:mr-20">
          <div className="flex">
            <div className="text-gray-200 pr-1">
              <WiHumidity className="text-3xl" />
            </div>
            <div>
              <div className="text-gray-200 text-sm sm:base">Humidity</div>
              <div className="text-white text-2xl sm:text-3xl font-bold">
                {timestamps[hour].humidity}%
              </div>
            </div>
          </div>

          <div className="flex my-4">
            <div className="text-gray-200 pr-1">
              <GiCrossedAirFlows className="text-2xl" />
            </div>
            <div>
              <div className="text-gray-200 text-sm sm:base">Air Pressure</div>
              <div className="text-white text-2xl sm:text-3xl font-bold">
                {timestamps[hour].pressure} hPa
              </div>
            </div>
          </div>

          <div className="flex my-4">
            <div className="text-gray-200 pr-1">
              <WiStrongWind className="text-2xl" />
            </div>
            <div>
              <div className="text-gray-200 text-sm sm:base">Wind speed</div>
              <div className="text-white text-2xl sm:text-3xl font-bold">
                {timestamps[hour].windSpeed} km/h
              </div>
            </div>
          </div>

          <div className="flex my-4">
            <div className="text-gray-200 pr-1">
              <MdVisibility className="text-2xl" />
            </div>
            <div>
              <div className="text-gray-200 text-sm sm:base">Visibility</div>
              <div className="text-white text-2xl sm:text-3xl font-bold">
                {timestamps[hour].visibility}%
              </div>
            </div>
          </div>
        </div>
      </div>

      <div className="w-screen text-white" style={{ height: "35%" }}>
        <div className="flex items-center pl-2 sm:pl-8">
          {determineNext5Days().map((e, i) => {
            return (
              <div
                className="px-2 py-1 mx-2 lg:mb-2 rounded-lg cursor-pointer"
                style={day === i ? blurredChip : {}}
                onClick={() => {
                  setHour(0);
                  setDay(i);
                }}
                key={i}
              >
                {e}
              </div>
            );
          })}
        </div>

        <div className="flex justify-around px-8 pt-6 sm:pt-5">
          {timestamps.map((e: any, index: number) => {
            return (
              <div
                key={index}
                className="h-40 w-40 flex flex-col cursor-pointer"
                style={{
                  boxShadow: "0 0 15px 1px rgba(0, 0, 0, 0.75)",
                  backdropFilter: "blur(2px)",
                  transform: hour === index ? "scale(1.1)" : "",
                  zIndex: hour === index ? 2 : 1,
                }}
                onClick={() => setHour(index)}
              >
                <div className="pt-2 pl-2">{e.time}</div>
                <div className="flex-grow"></div>
                <div className="pl-1 sm:pl-2 pb-1 sm:pb-2">
                  <div className="text-2xl font-bold">{e.temp}°C</div>
                  {hour === index ? (
                    <div className="text-xs sm:text-base">
                      Feels like {e.feelsLike}°
                    </div>
                  ) : null}
                </div>
              </div>
            );
          })}
        </div>
      </div>
    </>
  );
}

export default Desktop;
```

**Challenge** for you can be to separate this huge chunk of code into **smaller** components.

Welcome to **React Hook**. The hooks are amazing. I was wondering why the dev community makes all this drama about hooks. I didn't know anything about React back then. But after learning, I realised that it's a nice developer experience.

Here is the power of Javascript - **callbacks**.

**Challenge** for you could be to show the time of these cities. They are not in the same timezone, so its gonna be interesting.

Life without high-order functions would be painful.

****

## Step 10

#### Utils.tsx



There is a lot of functionality that needs to be shared between components. Don't clutter code with duplications.

The functionality that we will adapt according to [API](https://openweathermap.org/weather-conditions) is changing icons and gifs.

It's hardcoded. If the project was real-life, it will be through RegEx and loops. But for this purpose, the switch will do the job.

To not clutter already long post, [**here is the code of Utils.tsx**](https://github.com/kenosej/weather-react/blob/develop/src/components/Utils.tsx). Path: *./src/components/Utils.tsx*

****

## Step 11

#### Prepare for production



*./postcss.config.js*

```js
const purgecss = require("@fullhuman/postcss-purgecss")({
  content: [
    "./src/**/*.html",
    "./src/**/*.ts",
    "./src/**/*.tsx",
    "./public/index.html",
  ],

  defaultExtractor: (content) => {
    const broadMatches = content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || [];

    const innerMatches = content.match(/[^<>"'`\s.()]*[^<>"'`\s.():]/g) || [];

    return broadMatches.concat(innerMatches);
  },
});

const cssnano = require("cssnano");

module.exports = {
  plugins: [
    require("tailwindcss"),
    require("autoprefixer"),
    cssnano({
      preset: "default",
    }),
    purgecss,
  ],
};
```

*./package.json*

```
"build:tailwind": "tailwind build src/tailwind.css -o src/tailwind.output.css"
```

**change** to

```
"build:tailwind": "postcss src/tailwind.css -o src/tailwind.output.css"
```

Run `npm run build` and you will get rid of the unused Tailwind classes and end up with **~3kb** CSS file.

There's an option for passing ENV argument into `npm build` and minimizing CSS only for production, but let's keep it simple here.

You may serve production build with the static server. You should receive a manual in the terminal after `npm run build`.

Voila!

****

## Backstory



Why I built this application?

- To get a taste of React, Redux, Typescript, and Tailwind. I've learned those in 3 days.

Why Redux in the ultra-small application?

* To find out why the whole Internet complains about Redux... but it's not that scary!

Why bother posting it?

- Someone is gonna find it useful. More content - better.

  

Can't wait to learn more about these web technologies. 🧐

****

## The end

I hope you learned something from my **first** post. I thought that post would be much shorter. Even in a simple application is hard to cover all parts.

**Thank you** for reading. I hope I **helped** you. A well thought out **critique** is welcome.