Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET / ASP.NET-Core

Tour of Heroes: React, with ASP.NET Core Backend

3.22/5 (3 votes)
20 Nov 2023CPOL4 min read 3.1K  
A series of articles comparing programmer experiences of Angular, Aurelia, React, Vue, Xamarin and MAUI
Tour of Heroes on React, talking to a real backend through generated client API

Background

"Tour of Heroes" is the official tutorial app of Angular 2+. The app contains some functional features and technical features which are common when building a real-world business application:

  1. A few screens presenting tables and nested data
  2. Data binding
  3. Navigation
  4. CRUD operations over a backend, and optionally through a generated client API
  5. Unit testing and integration testing

In this series of articles, I will demonstrate the programmer experiences of various frontend development platforms when building the same functional features: "Tour of Heroes", a fat client talking to a backend.

The frontend apps on Angular, Aurelia, React, Vue, Xamarin and MAUI are talking to the same ASP.NET (Core) backend through generated client APIs. To find the other articles in the same series, please search "Heroes" in my articles. And at the end of the series, some technical factors of programmer experiences will be discussed:

  1. Computing science
  2. Software engineering
  3. Learning curve
  4. Build size
  5. Runtime performance
  6. Debugging

Choosing a development platform involve a lot of non-technical factors which won't be discussed in this series.

References:

Introduction

This article is focused on React TS.

Development platforms

  1. Web API on ASP.NET Core 8
  2. Frontend on React 18.2.0

Demo Repository

Checkout DemoCoreWeb in GitHub, and focus on the following areas:

Core3WebApi

ASP.NET Core Web API.

React Heroes

This is a rewrite of the official tutorial demo of Angular "Tour of Heroes", for side by side comparison based on the same functional features.

Remarks

DemoCoreWeb was established for testing NuGet packages of WebApiClientGen and demonstrating how to use the libraries in real world projects.

Using the Code

Prerequisites

  1. Core3WebApi.csproj has NuGet packages "Fonlow.WebApiClientGenCore" and "Fonlow.WebApiClientGenCore.Axios" imported.
  2. Add CodeGenController.cs to Core3WebApi.csproj.
  3. Core3WebApi.csproj has CodeGen.json . This is optional, just for the convenience of running some PowerShell script to generate client APIs.
  4. CreateWebApiClientApi3.ps1. This is optional. This script will launch the Web API on DotNet Kestrel web server and post the data in CodeGen.json.

Remarks

Depending on your CI/CD process, you may adjust item 3 and 4 above. For more details, please check:

Also, if your React TS project uses Fetch API, you may import WebApiClientGen plugin Fonlow.WebApiClientGenCore.Fetch.

Generate Client API

Not like Angular and Aurelia which are TS/JS framework, React is a JS library which does not include built-in HTTP client. And React programmers typically would use Fetch API or AXIOS. In React Heroes, AXIOS is used.

In CodeGen.json, include the following:

JavaScript
"Plugins": [
			{
				"AssemblyName": "Fonlow.WebApiClientGenCore.Axios",
				"TargetDir": "..\\..\\..\\..\\ReactHeroes\\src\\clientapi",
				"TSFile": "WebApiCoreAxiosClientAuto.ts",
				"AsModule": true,
				"ContentType": "application/json;charset=UTF-8"
			},

Run "CreateWebApiClientApi3.ps1", the generated codes will be written to "WebApiCoreAxiosClientAuto.ts".

Data Models and API Functions

TypeScript
export namespace DemoWebApi_Controllers_Client {

    /**
     * Complex hero type
     */
    export interface Hero {
        id?: number | null;
        name?: string | null;
    }

}
TypeScript
export class Heroes {
    constructor(private baseUri: string = window.location.protocol +
     '//' + window.location.hostname + (window.location.port ? ':' +
      window.location.port : '') + '/') {
    }

    /**
     * DELETE api/Heroes/{id}
     */
    delete(id: number | null, headersHandler?: () =>
               {[header: string]: string}): Promise<AxiosResponse> {
        return Axios.delete(this.baseUri + 'api/Heroes/' + id,
               { headers: headersHandler ? headersHandler() : undefined });
    }

    /**
     * Get a hero.
     * GET api/Heroes/{id}
     */
    getHero(id: number | null, headersHandler?: () =>
     {[header: string]: string}): Promise<DemoWebApi_Controllers_Client.Hero> {
        return Axios.get<DemoWebApi_Controllers_Client.Hero>
          (this.baseUri + 'api/Heroes/' + id, { headers: headersHandler ?
                 headersHandler() : undefined }).then(d => d.data);
    }

    /**
     * Get all heroes.
     * GET api/Heroes
     */
    getHeros(headersHandler?: () => {[header: string]: string}):
             Promise<Array<DemoWebApi_Controllers_Client.Hero>> {
        return Axios.get<Array<DemoWebApi_Controllers_Client.Hero>>
               (this.baseUri + 'api/Heroes', { headers: headersHandler ?
                headersHandler() : undefined }).then(d => d.data);
    }

    /**
     * POST api/Heroes
     */
    post(name: string | null, headersHandler?: () =>
      {[header: string]: string}): Promise<DemoWebApi_Controllers_Client.Hero> {
        return Axios.post<DemoWebApi_Controllers_Client.Hero>
        (this.baseUri + 'api/Heroes', JSON.stringify(name),
        { headers: headersHandler ? Object.assign(headersHandler(),
        { 'Content-Type': 'application/json;charset=UTF-8' }):
        { 'Content-Type': 'application/json;charset=UTF-8' } }).then(d => d.data);
    }


    /**
     * Update hero.
     * PUT api/Heroes
     */
    put(hero: DemoWebApi_Controllers_Client.Hero | null,
        headersHandler?: () => {[header: string]: string}):
        Promise<DemoWebApi_Controllers_Client.Hero> {
        return Axios.put<DemoWebApi_Controllers_Client.Hero>
               (this.baseUri + 'api/Heroes', JSON.stringify(hero),
               { headers: headersHandler ? Object.assign(headersHandler(),
               { 'Content-Type': 'application/json;charset=UTF-8' }):
               { 'Content-Type': 'application/json;charset=UTF-8' } }).then(d => d.data);
    }
}

While there could be multiple ways of utilizing the generated API functions, the official React tutorial or documentation does not seem to suggest, recommend or mandate a predefined way of utilizing HTTP client calls. I am not sure what could be the orthodox way or the popular way by seasoned React programmers when crafting complex business applications. Here's what I would do:

TypeScript
import { DemoWebApi_Controllers_Client } from './clientapi/WebApiCoreAxiosClientAuto';

export  let HeroesApi = heroesApi();
function heroesApi() {
  const apiBaseUri = 'http://localhost:5000/';
  const service = new DemoWebApi_Controllers_Client.Heroes(apiBaseUri);
  return service;
}

Remarks

  • If you are a seasoned React programmer, please advise a better, orthodox or popular way of utilizing generated client API. Your advice will be greatly appreciated.

Views

Editing

HeroDetail.tsx

JavaScript
import { useEffect, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import './HeroDetail.css';
import { DemoWebApi_Controllers_Client } from './clientapi/WebApiCoreAxiosClientAuto';
import { HeroesApi } from './HeroesApi';

export default function HeroDetail() 
 { //https://stackoverflow.com/questions/47561848/property-value-does-not-exist-on-type-readonly
  const service = HeroesApi
  const { id } = useParams();
  const [hero, setHero] = useState<DemoWebApi_Controllers_Client.Hero | undefined>(undefined);
  const heroId: any = id;
  const navigate = useNavigate();
  const nameInput = useRef<HTMLInputElement>(null);

  useEffect(() => {
    console.debug('getHero...');
    service.getHero(heroId).then(
      h => {
        if (h) {
          setHero(h);
        }
      }
    ).catch(error => alert(error));
  }, []); //empty array to run only once. But in dev mode, 
          //it will run twice, since the cmponent is mounted twice. 
          //https://stackoverflow.com/questions/72238175/why-useeffect-running-twice-and-how-to-handle-it-well-in-react

  if (!hero) {
    return <div>AAA</div>;
  }

  function save(): void {
    service.put(hero!).then(
      d => {
        setHero({...hero})
        console.debug('response: ' + JSON.stringify(d));
      }
    ).catch(error => alert(error));
  }

  function goBack(): void {
    navigate(-1);
  }

  function handleChange(e: React.FormEvent<HTMLInputElement>) {
    hero!.name = e.currentTarget.value;
    setHero({...hero});
  }

  return (
    <div className="hero-detail">
      <h2>{hero.name} Details</h2>
      <div><span>id: </span>{hero!.id}</div>
      <div>
        <label htmlFor="hero-name">Hero name: </label>
        <input id="hero-name" value={hero.name!} 
         placeholder="Name" onChange={handleChange} ref={nameInput} />
      </div>

      <button type="button" onClick={goBack}>go back</button>
      <button type="button" onClick={save}>save</button>
    </div>
  );
}

Heroes List

Heroes.tsx

TypeScript
import { useEffect, useRef, useState } from 'react';
import './Heroes.css';
import { DemoWebApi_Controllers_Client } from './clientapi/WebApiCoreAxiosClientAuto';

import { Link, useNavigate } from 'react-router-dom';
import {HeroesApi} from './HeroesApi';

export default function Heroes() {
  const service = HeroesApi;
  const [heroes, setHeroes] = useState<DemoWebApi_Controllers_Client.Hero[]>([]);
  const [selectedHero, setSelectedHero] = 
         useState<DemoWebApi_Controllers_Client.Hero | undefined>(undefined);
  const navigate = useNavigate();
  const heroNameElement = useRef<HTMLInputElement>(null);

  useEffect(() => {
    console.debug('getHeros...');
    service.getHeros().then(
      data => {
        setHeroes(data);
      }
    ).catch(error => console.error(error))
  }, []); //empty array to run only once. But in dev mode, it will run twice, 
          //since the cmponent is mounted twice. 
          //https://stackoverflow.com/questions/72238175/why-useeffect-running-twice-and-how-to-handle-it-well-in-react

  return (
    <>
      <h2>My Heroes</h2>
      <div>
        <label htmlFor="new-hero">Hero name: </label>
        <input id="new-hero" ref={heroNameElement} />
        <button type="button" className="add-button" onClick={addAndClear}>
          Add hero
        </button>
      </div >

      <ul className="heroes">
        {
          heroes.map((h) =>
            <li>
              <Link to={`/detail/${h.id}`} key={h.id}>
                <span className="badge">{h.id}</span> {h.name}
              </Link>
              <button type="button" className="delete" title="delete hero" 
                      onClick={() => deleteHero(h)}>x</button>
            </li >)
        }
      </ul >
    </>

  );

  function onSelect(hero: DemoWebApi_Controllers_Client.Hero): void {
    setSelectedHero(hero);
  }

  function gotoDetail(): void {
    navigate(`/detail/${selectedHero?.id}`);
  }

  function addAndClear() {
    add(heroNameElement.current?.value);
    heroNameElement.current!.value = '';
  }

  function deleteHero(hero: DemoWebApi_Controllers_Client.Hero): 
           void { //delete is a reserved word in React
    service.delete(hero.id!).then(
      () => {
        setHeroes(heroes?.filter(h => h !== hero));
        if (selectedHero === hero) { setSelectedHero(undefined); }
      });
  }

  function add(name: string | undefined): void {
    if (!name) { return; }

    name = name.trim();
    service.post(name).then(
      hero => {
        setHeroes([...heroes, hero]);
        console.debug('hero added: ' + heroes.length);
        setSelectedHero(undefined);
      });
  }
}

View Models

HeroDetail.tsx has implemented two-way binding for the input element through some lengthy app codes, however, there's no automatic update to:

HTML
<h2>{hero.name} Details</h2>

after hero.name is altered. More lengthy app codes are expected to update. This is because React is a JS library, not a JS framework like Aurelia which provides built-in MVVM architecture, while two-way binding is a basic feature of any MVVM architecture. Proficient React programmers have been using wide variety of ways for MVVM.

Routing

React provides built-in routing mechanism which is abstract enough.

Symbolic routing globally or within a module

TypeScript
function AppRouteMap() {
  return useRoutes([
    { path: 'demo', element: <Demo /> },
    { path: '/', element: <Home /> },
    {
      element: <Home />,
      children: [
        { path: 'dashboard', element: <Dashboard /> },
        { path: 'heroes', element: <Heroes /> },
        { path: 'detail/:id', element: <HeroDetail /> }
      ]
    }
  ]);

}

export default function App() {
  return (
    <BrowserRouter>
      <AppRouteMap />
    </BrowserRouter>
  );
}
HTML
<Link to={`/detail/${h.id}`} key={h.id}>
  <span className="badge">{h.id}</span> {h.name}
</Link>

Integration Testing

Since the frontend of "Tour of Heroes" is a fat client, significant portion of the integration testing is against the backend. Two examples:

Points of Interests

DemoCoreWeb in GitHub is initially created for testing released packages of WebApiClient, it also serves well the following purposes:

  1. A demo not too simple and not too complex for various development platforms. After you learn one, it should be easy for you to learn the others, based on the same business functionality.
  2. Demonstrating programmer experience of using various development platforms, to give you some ideas when choosing a development platform for your next project, based on your business content and context.

The backend provides multiple sets of Web API, while "Tour of Heroes" consumes only those exposed in HeroesController. In the real world, a backend may serve multiple frontend apps, while a front end app may talk to multiple backends.

Even though I had written this React Heroes app, I have never used React for commercial projects, thus haven't deep-dive enough to really understand React and its technical landscape. There may be some misconceptions about React in this article. Please leave your comment if you are a seasoned React programmer, and I would greatly appreciate it.

History

  • 20th November, 2023: Initial version

License

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