[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.

    59

    This website collects cookies to deliver better user experience

    [TypeScript][Express] Try React