Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / React

Global Weather - React App with ASP.NET Core 3.0 (Part 2)

4.89/5 (20 votes)
12 Dec 2019CPOL7 min read 40.4K   553  
Learn how to integrate rest API calls into React-Redux for client-side state management, using Redux to manage the data instead of directly accessing component state.

Introduction

I’ve introduced how to build React App with .NET Core in Part 1. Now we start to touch the fun part, React-Redux. There are states everywhere in an application. Data morphing over time involves complexity. To handle state in React, Redux is often used as the solution. In this article, we’ll rewrite Global Weather with react-redux step by step.

Redux

Redux is a state management tool. While it’s mostly used with React, it can be used with any other JavaScript/TypeScript framework or library. In an app, the data in a component should live in just one component. So sharing data among sibling components becomes difficult.

For instance, in React, to share data among siblings, a state has to live in the parent component. A method for updating this state is provided by this parent component and passed as props to these sibling components. This is exactly what happens in our Global Weather React app. Weather View Model stays in Home component (parent component), and pass it to WeatherDetails component as properties. The state will have to be lifted up to the nearest parent component and to the next until it gets to an ancestor that is common to both components that need the state and then it is passed down. This makes the state difficult to maintain and less predictable. This is why we need a state management tool like Redux that makes it easier to maintain these states.

There are three parts in Redux: actions, store and reducers.

  1. Actions are events. They are the way to send data from application to Redux store.
  2. Reducers are functions that take the current state of an application, perform an action and returns a new state.
  3. The store holds the application state. There is only one store in a Redux application.

Now we have concepts of Redux. Let’s rewrite Global Weather app with Redux.

Visual Studio Code

In this article, coding will be focused on front end React, “weatherclient”. So I’ll use Visual Studio Code rather than Visual Studio. Visual Studio Code is a lightweight but powerful source code editor which runs on your desktop and is available for Windows, macOS and Linux. It comes with built-in support for JavaScript, TypeScript and Node.js and has a rich ecosystem of extensions for other languages (such as C++, C#, Java, Python, PHP, Go) and runtimes (such as .NET and Unity). You can download Visual Studio Code from here.

Open weatherclient folder from Visual Studio Code.

Image 1

Run npm install in powershell terminal.

Then go to parent folder, “GlobalWeatherReact”, to start ASP.NET core from command line.

PowerShell
dotnet run

Install “Debugger Chrome” plugin, and add launch configuration.

Image 2

Click Debug button, GlobalWeather app starts running with Chrome.

Image 3

It’s very easy to debug typescript/javascript code from Visual Studio Code. I’ll show you how to do it later.

Install React-Redux and Middleware

Go to “weatherclient” folder to install libraries we need.

React-Redux

React-Redux is official react binding for redux.

PowerShell
npm install react-redux @types react-redux --save

Redux-thunk

Thunks are the recommended middleware for basic Redux side effects logic, including complex synchronous logic that needs access to the store, and simple async logic like AJAX requests. Redux Thunk middleware allows you to write action creators that return a function instead of an action. The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met.

PowerShell
npm install redux-thunk --save

Axios and redux-axios-middlware

Axios is a library to make http request. As with Fetch, Axios is promise-based. However, it provides a more powerful and flexible feature set. Advantages over the native Fetch API include:

  • Request and response interception
  • Streamlined error handling
  • Protection against XSRF
  • Support for upload progress
  • Response timeout
  • The ability to cancel requests
  • Support for older browsers
  • Automatic JSON data transformation

Redux-axios-middleware

Redux middleware for fetching data with axios HTTP client.

PowerShell
npm install axios redux-axios-middleware --save

Creating Actions

From the work we’ve already done, we know that our state needs to have 2 properties of state, countries, and current weather for the specified location.

So let’s started create action types. Create an types/actions.ts file to container our action types.

JavaScript
import { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
import { Weather } from "../types/Weather"
export type AppActions =
    {
        type: 'GET_WEATHER',
    } | {
        type: 'GET_WEATHER_SUCCESS',
        payload: Weather
    } | {
        type: 'GET_WEATHER_FAIL',
        error: string
    }  | {
        type: 'GET_COUNTRIES',
        payload: {
            request: AxiosRequestConfig
            }
    } | {
        type: 'GET_COUNTRIES_SUCCESS',
        payload: AxiosResponse
    } | {
        type: 'GET_COUNTRIES_FAIL',
        error: AxiosError
    };
export const GET_WEATHER = 'GET_WEATHER';
export const GET_WEATHER_SUCCESS = 'GET_WEATHER_SUCCESS';
export const GET_WEATHER_FAIL = 'GET_WEATHER_FAIL';
export const GET_COUNTRIES = 'GET_COUNTRIES';
export const GET_COUNTRIES_SUCCESS = 'GET_COUNTRIES_SUCCESS';
export const GET_COUNTRIES_FAIL = 'GET_COUNTRIES_FAIL';

Then create an actions/actions.ts file to container our action creators.

getCountries action creator:

JavaScript
import { AppActions } from "../types/actions";
import { Constants } from "../Constants";
export const getCountries = (): AppActions => ({
    type: "GET_COUNTRIES",
    payload: {
        request: {
            url: `${Constants.locationAPIUrl}/countries?apikey=${Constants.apiKey}`
        }
    }
});

With redux axios middleware, we just need configure axios request with the correct url.

getWeather action creator:

JavaScript
export const getWeather = (countryCode: string, searchText: string) => {
    return async (dispatch: ThunkDispatch<any, any, AppActions>) => {
        try {
            var res = await Axios.get(`${Constants.locationAPIUrl}/cities/
                      ${countryCode}/search?apikey=${Constants.apiKey}&q=${searchText}`);
            const cities = res.data as City[];
            var weather = {} as Weather;
            if (cities.length > 0) {
                const city = cities[0];
                weather = await getCurrentWeather(city);
            }

            return dispatch({
                type: "GET_WEATHER_SUCCESS",
                payload: weather
            });
        }
        catch (e) {
            return dispatch({
                type: "GET_WEATHER_FAIL",
                error: e.isAxiosError ? e.message : JSON.stringify(e)
            });
        }
    }
}
export async function getCurrentWeather(city: City): Promise<Weather> {
    const res = await Axios.get(`${Constants.currentConditionsAPIUrl}/
                               ${city.Key}?apikey=${Constants.apiKey}`);
    const currentConditions = res.data as CurrentCondition[];
    if (currentConditions.length > 0) {
        return new Weather(currentConditions[0], city);
    }
    return {} as Weather;
}

The code logic is not new, just combine some functions was in Home component. But you can see we’re using Thunk Dispatch here. A thunk is a function that wraps an expression to delay its evaluation. Return value from the action creator will be return value of dispatch itself. This is convenient for orchestrating an asynchronous control flow with thunk action creators dispatching each other and returning Promises to wait for each other’s completion.

Also, this creator function returns different action type and payload depends on the fetch result.

Creating Reducers

With action creators defined, we now write reducers that take this action and return a new state of our application.

Create a reducers/countriesReducer.ts file.

JavaScript
import { Country } from "../types/Country";
import { AppActions } from "../types/actions";
import * as Actions from "../types/actions";

const countriesReducerDefaultState: Country [] = [];

const countriesReducer = (state = countriesReducerDefaultState, action: AppActions): 
         Country [] => {
    switch (action.type) {
        case Actions.GET_COUNTRIES_SUCCESS:
            const data = action.payload.data;
            return data as Country [];
        case Actions.GET_COUNTRIES_FAIL:
            return state;
        default:
            return state;
    }
};
export { countriesReducer };

Create reducers/weatherReducer.ts file.

JavaScript
import { Weather } from "../types/Weather";
import { AppActions } from "../types/actions";
import * as Actions from "../types/actions";
const weatherReducerDefaultState: Weather = {};
const weatherReducer = (state = weatherReducerDefaultState, action: AppActions): Weather => {
    switch (action.type) {
        case Actions.GET_WEATHER_SUCCESS:
            const weather = action.payload;
            return weather;

        case Actions.GET_WEATHER_FAIL:
            return {
                error: action.error
            } as Weather;

        default:
            return state;
    }
};
export { weatherReducer };

Configure the Store

Essentially, the redux store does the following:

  • Holds application state
  • Allows access to state via getState(). The getState method returns the current state tree of your app.
  • Allows state to be updated via dispatch(action). The dispatch method “dispatches” an action, consequently triggering a state change.
  • Registers listeners via subscribe(listener). The subscribe(listener) adds a change listener.
  • Handles unregistering of listeners via the function returned by subscribe(listener)

Let’s create store/configureStore.ts with:

JavaScript
import axios, {AxiosRequestConfig} from 'axios';
import thunk, { ThunkMiddleware } from "redux-thunk";
import { createStore, combineReducers, applyMiddleware } from "redux";
import axiosMiddleware from 'redux-axios-middleware';
import { AppActions } from "../types/actions";
import { countriesReducer } from "../reducers/countriesReducer";
import { weatherReducer } from "../reducers/weatherReducer";
export const rootReducer = combineReducers({
    countries: countriesReducer,
    weather: weatherReducer
});
const config: AxiosRequestConfig = {
    responseType: 'json'
};
const defaultClient = axios.create(config);
export type AppState = ReturnType<typeof rootReducer>;
export const store = createStore(rootReducer, applyMiddleware(axiosMiddleware(defaultClient),
                     thunk as ThunkMiddleware<AppState, AppActions>));

Now change our app’s App.tsx to include <Provider />, configureStore, set up our store and wrap our app to pass the store down as props:

JavaScript
import React from 'react';
import "./App.css";
import { Provider } from "react-redux";
import { store } from './store/configureStore';
import AppRouter from "./router";
const App: React.FC = () => {
    return (
        <Provider store={store}>
            <AppRouter />
        </Provider>
    );
}
export default App;

Use the Redux Store and Methods in Components

Now we need map action creation dispatch to component, and connect app state to component.

Home Component

Before we using redux, we fetch all countries in ComponentDidMount of Home component, then pass country array to Form component as property.

Below is the current render method of Home component. Different components sharing data via the parent component.

JavaScript
render() {
    return (
        <div className="container content panel">
            <div className="container">
                <div className="row">
                    <div className="form-container">
                        <WeatherDetails weather={this.state.weather} />
                        <Form getWeather={this.getWeather} countries={this.state.countries} />
                    </div>
                </div>
            </div>
        </div>
    );
}

With redux, we don’t need this approach any more. The component can connect to app state (shared data) from itself.

We change render method as the below:

JavaScript
render() {
        return (
            <div className="container content panel">
                <div className="container">
                    <div className="row">
                        <div className="form-container">
                            <WeatherDetails />
                            <Form  />
                        </div>
                    </div>
                </div>
            </div>
        );
}

And also remove all API methods, like getCountries, getCity, getWeather, … We’ll implement these functionalities with redux actions.

First, add IDispatch interface for Home component.

JavaScript
interface IDispatchProps {
    getCountries: () => void;
}

Then change Home component to extend this properties interface.

JavaScript
class Home extends React.Component<IDispatchProps, IState>

Connect is what allows us to connect a component to Redux's store, and getCountries is the action creator we wrote earlier.

Map IDispatchProp to Action Creator with Thunk middleware.

JavaScript
class Home extends React.Component<IDiconst mapDispatchToProps = 
    (dispatch: ThunkDispatch<any, any, AppActions>): IDispatchProps => ({
    getCountries: bindActionCreators(getCountries, dispatch),
});
export default connect(null, mapDispatchToProps)(Home);

Form Component

Form component needs to do two things. Map AppState.countries to the form property, and connect getWeather action.

JavaScript
interface IFormProps {
    countries: Country[];
}
interface IDispatchProps {
    getWeather: (country: string, city: string) => void;
}
interface IProps extends IFormProps, IDispatchProps {}

Connect to Form component.

JavaScript
export default connect (mapStateToProps, mapDispatchToProps) (Form)

Change handleSubmit function to use dispatch property.

JavaScript
handleSubmit = async (e: any) => {
        e.preventDefault();
        if (this.state.searchText && this.state.country)
            this.props.getWeather(this.state.country.ID, this.state.searchText);
}

WeatherDetails Component

Map AppState.weather to component property in WeatherDetails component.

JavaScript
const mapStateToProps = (state: AppState) : IProp =>({
    weather : state.weather
});
export default connect (mapStateToProps) (WeatherDetails)

That’s it. Run “npm run build” in terminal to make sure all is compilable.

Image 4

Debug React App in Visual Studio Code

As I said before, install “Debugger for Chrome” plugin and create a Json configuration. For example:

XML
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "chrome",
            "request": "launch",
            "name": "Launch Chrome against localhost",
            "url": "https://localhost:5001",
            "webRoot": "${workspaceFolder}"
        }
    ]
}

Now you need start server first, for our app is .NetCore API. We can do it in command line.

Go to GloablWeatherReact (ASP.NET Core) project folder, run “dotnet run”.

Image 5

Image 6

You can see the development server is started at https://localhots:5001 and http://localhost:5000.

That gives you an idea of how to set url in Chrome Debugger configuration.

You can check all launch settings from GloblaWeatherReact/properties/launchSettings.json.

Now we set breakpoints at actions/actions.ts. Then click the launch button.

Image 7

When you select a country and input location name, click “Go”, the break point is getting triggered.

Image 8

Image 9

You can see call stack and add whatever object to watch.

Ok. Let it go, click Continue button, or press “F5”.

Image 10

It’s working perfectly.

GlobalWeatherReact GitHub

Global Weather React App now is available on GitHub. The master branch is implemented by react, and redux branch is for react-redux implementation.

Conclusion

In this article, we learned how to integrate rest API calls into React-Redux for client-side state management, using Redux to manage the data instead of directly accessing component state. Keep in mind that the whole purpose of Redux is to manage a global state for us, and gives react app really cool superpowers. We also learned how to use an awesome VS Code plugin for debugging React-Redux apps.

History

  • 20th November, 2019: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)