[TypeScript][Express] Try React

Intro

This time, I will tye React to get and show book data from the Express application.

Environments

  • Node.js ver.16.5.0
  • create-react-app ver.4.0.3
  • React ver.17.0.2
  • react-router-dom ver.5.2.0
  • TypeScript ver.4.3.5
  • ESLint ver.7.30.0

According to the tutorials, I created a React project by create-react-app.

npx create-react-app bookstore-sample-client --template typescript

Use ESLint

This time, I also try using ESLint.

npx eslint --init

And I got errors in TSX files.

'React' must be in scope when using JSX

According to some posts, I add a rule in .eslitrc.yml

.eslitrc.yml

env:
  browser: true
  es2021: true
extends:
  - 'eslint:recommended'
  - 'plugin:react/recommended'
  - 'plugin:@typescript-eslint/recommended'
parser: '@typescript-eslint/parser'
parserOptions:
  ecmaFeatures:
    jsx: true
  ecmaVersion: 12
  sourceType: module
plugins:
  - react
  - '@typescript-eslint'
rules: {
  "react/react-in-jsx-scope": "off",
}

Routing

Although I just add only one page in this sample, I try routing as a SPA by "react-router-dom".

[Client] App.tsx

import './App.css';
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";
import { SearchBooks } from './search/SearchBooks';

function App(): JSX.Element {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
          </ul>
        </nav>
        <Switch>
          <Route path="/">
            <SearchBooks />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}
export default App;

[Express] Use CORS

Because the domains aren't same as the Express app and the React app, I have to allow origins in the Express app.

[Server] index.ts

import "reflect-metadata";
import express from 'express';
import cors from 'cors';
import { container } from 'tsyringe';
import { BookService } from "./books/bookService";

const port = 3099;
const app = express();

const allowlist = ['http://localhost:3000', 'http://localhost:3099']
const corsOptionsDelegate: cors.CorsOptionsDelegate<any> = (req, callback) => {
  const corsOptions = (allowlist.indexOf(req.header('Origin')) !== -1)? { origin: true }: { origin: false };
  callback(null, corsOptions);
};
...
app.get('/books', cors(corsOptionsDelegate), async (req, res) => {
    const books = container.resolve(BookService);
    res.json(await books.getBooks());

});
app.listen(port, () => {
    console.log(`Example app listening at http://localhost:${port}`)
});

[Client] BookAccessor.ts

import { Book } from "../models/book";

export async function search(): Promise<Book[]> {    
    return await fetch('http://localhost:3099/books', {
        method: 'GET',
    })
    .then(response => response.json())
    .then(json => JSON.parse(JSON.stringify(json)))
    .catch(err => console.error(err));
}

Create pages

This time, I just add simple pages.

[Client] SearchBooks.tsx

import { useState } from "react";
import './SearchBooks.css';
import { Book } from "../models/book";
import { SearchBookRow } from "./SearchBookRow";
import * as bookAccessor from './BookAccessor';

export function SearchBooks(): JSX.Element {
    const [books, setBooks] = useState([] as Array<Book>);
    const generateRows = () => {
        const contents: JSX.Element[] = [];
        for(const b of books) {
            contents.push(<SearchBookRow key={b.id} book={b}></SearchBookRow>);
        }
        return contents;
    };
    return <div className="search_result_area">
        <button onClick={async () => {
            setBooks(await bookAccessor.search());
        }}>Search</button>
        {generateRows()}
    </div>
}

[Client] SearchBooks.css

.search_result_row {
    background-color: aqua;
}

[Client] SearchBookRow.tsx

import { Book } from "../models/book";

export type SearchBookRowProps = {
    book: Book
};
export function SearchBookRow(props: SearchBookRowProps): JSX.Element {

    return <div className="search_result_row">
        <div className="search_result_row_cell">{props.book.name}</div>
        <div className="search_result_row_cell">{props.book.author.name}</div>
        <div className="search_result_row_cell">{props.book.genre.name}</div>
        <div className="search_result_row_cell">{props.book.price}</div>
    </div>
}

className

According to the result, the CSS class names what are generated from "className" won't be changed automatically.
So I can write child components' CSS in parent's CSS like the sample.

If I add same CSS code in child components' CSS files, the child components use them.

45