[TypeScript][Express] Try React 2
Tags: typescript, react, express
Published: 2021-08-13 16:48:34

## Intro
This time, I will try get and upload data to the Express application.

* [[TypeScript][PostgreSQL][TSyringe][Express]Try TypeORM 2](https://dev.to/masanori_msl/typescript-postgresql-tsyringe-express-try-typeorm-2-3bka)
* [[TypeScript][Express] Try React](https://dev.to/masanori_msl/typescript-express-try-react-1anc)

### Environments
* Node.js ver.16.6.1
* 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

## Load data on page loading
I can use "useEffect" for loading data on page loading.

#### [Server] bookService.ts
```ts
import { autoInjectable } from "tsyringe";
import { Connection, QueryRunner } from "typeorm";
import { ActionResult } from "../application.type";
import { DataContext } from "../data/dataContext";
import { Author } from "../entities/author";
import { Book } from "../entities/book";
import { Genre } from "../entities/genre";

@autoInjectable()
export class BookService {
    public constructor(private context: DataContext) {
    }
...
    public async getBookById(id: number): Promise<Book|null> {
        const connection = await this.context.getConnection();
        const result = await connection.getRepository(Book)
            .createQueryBuilder('book')
            .innerJoinAndSelect('book.genre', 'genre')
            .innerJoinAndSelect('book.author', 'author')
            .where('book.id = :id', { id })
            .getOne();
        return (result == null)? null: result;
    }
    public async getGenres(): Promise<readonly Genre[]> {
        const connection = await this.context.getConnection();
        return await connection.getRepository(Genre)
            .createQueryBuilder('genre')
            .getMany();
    }
    public async getAuthors(): Promise<readonly Author[]> {
        const connection = await this.context.getConnection();
        return await connection.getRepository(Author)
            .createQueryBuilder('author')
            .getMany();
    }
...
}
```
#### [Server] index.ts
```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);
};
// To receive JSON value from client-side
app.use(express.json());
app.use(express.static('clients/public'));
...
app.get('/genres', cors(corsOptionsDelegate), async (req, res) => {
  const books = container.resolve(BookService);
  res.json(await books.getGenres());
});
app.get('/authors', cors(corsOptionsDelegate), async (req, res) => {
  const books = container.resolve(BookService);
  res.json(await books.getAuthors());
});
app.listen(port, () => {
    console.log(`Example app listening at http://localhost:${port}`)
});
```
#### [Client] booksGetter.ts
```ts
import { Author } from "../models/author";
import { Book } from "../models/book";
import { Genre } from "../models/genre";

export async function getBookById(id: number): Promise<Book|null> {
    return await fetch(`http://localhost:3099/book/${id}`, {
        method: 'GET',
        mode: 'cors',
    })
    .then(response => response.json())
    .then(json => {
        console.log(json);
        
        return JSON.parse(JSON.stringify(json));
    })
    .catch(err => console.error(err));
}
export async function getAllGenres(): Promise<Genre[]> {
    return await fetch('http://localhost:3099/genres', {
        method: 'GET',
        mode: 'cors',
    })
    .then(response => response.json())
    .then(json => {        
        return JSON.parse(JSON.stringify(json));
    })
    .catch(err => {
        console.error(err);
        return [];
    });
}
export async function getExistedAuthors(): Promise<Author[]> {
    return await fetch('http://localhost:3099/authors', {
        method: 'GET',
        mode: 'cors',
    })
    .then(response => response.json())
    .then(json => {
        return JSON.parse(JSON.stringify(json));
    })
    .catch(err => {
        console.error(err);
        return [];
    });
}
```
#### [Client] EditBooks.tsx
```tsx
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import * as bookGetter from '../books/booksGetter';
import * as bookUploader from '../books/booksUploader';
import { Author } from '../models/author';
import { Genre } from '../models/genre';

const defaultAuthor = {
    id: -1,
    name: ''
};
const defaultBook = {
    id: -1,
    name: '',
    price: -1,
    authorId: defaultAuthor.id,
    author: defaultAuthor,
    genreId: -1,
    genre: {
        id: -1,
        name: ''
    },
    lastUpdateDate: new Date(),
};
export function EditBooks(): JSX.Element {
    const [book, setBook] = useState(defaultBook);
    const [genres, setGenres] = useState(new Array<Genre>());
    const [authors, setAuthors] = useState(new Array<Author>());
    const addGenres = () => {
        const contents: JSX.Element[] = [];
        for(const g of genres) {
            const key = `genre_${g.id}`;
            contents.push(<option key={key} value={g.id}>{g.name}</option>);
        }
        return contents;
    }
    useEffect(() => {        
        bookGetter.getAllGenres()
            .then(genres => setGenres(genres));
        bookGetter.getExistedAuthors()
            .then(authors => setAuthors(authors));
    }, []);
    return <div className="create_books_area">
        <h2>Create</h2>
        <div className="create_books_row">
            <div className="create_books_item_area">
                <div className="create_books_item_title">Genre</div>
                <select defaultValue={book.genreId}>
                    {addGenres()}
                </select>
            </div>
        </div>
        <div className="create_books_row">
            <div className="create_books_item_area">
                <div className="create_books_item_title">Name</div>
                <input type="text" placeholder="Name" defaultValue={book.name}></input>
            </div>
            <div className="create_books_item_area">
                <div className="create_books_item_title">Author</div>
                <input type="text" placeholder="Author" defaultValue={book.author?.name}></input>
            </div>
            <div className="create_books_item_area">
                <div className="create_books_item_title">Price</div>
                <input type="number" placeholder = "Price" defaultValue={book.price}></input>
            </div>
            <div className="create_books_item_area">
                <button onClick={() => console.log('save')}>Save</button>
            </div>
        </div>
    </div>
}
```

Because I want to load data only on page loading, I set "[]" as the second argument.

One important thing is I can't use async/await in "useEffect" because it required "void" as return value.

* [Using the Effect Hook - React](https://reactjs.org/docs/hooks-effect.html)

## Update value
For updating "Book" data by input data, I update states when "onChange" events are fired.

#### [Client] EditBooks.tsx
```tsx
...
export function EditBooks(): JSX.Element {
...
    const updateBook = (propertyName: 'name'|'authorName'|'price', value: string|undefined) => {
        let updateAuthor: Author|undefined|null = null;
        switch(propertyName) {
            case 'name':
                    book.name = (value == null)? '': value;
                break;
            case 'authorName':
                    if(value == null) {
                        book.author = defaultAuthor;
                    } else {
                        updateAuthor = authors.find(a => a.name === value);
                        book.author = (updateAuthor == null)? 
                            generateNewAuthor(value): updateAuthor;
                    }
                    book.authorId = book.author.id;
                break;
            case 'price':
                book.price = getFloatValue(value);
                break;
        } 
        setBook(book);
    }
...
    return <div className="create_books_area">
        <h2>Create</h2>
        <div className="create_books_row">
            <div className="create_books_item_area">
                <div className="create_books_item_title">Genre</div>
                <select defaultValue={book.genreId.toString()} onChange={(e) => {
                    book.genreId = getIntValue(e.target.options[e.target.selectedIndex].value);
                    setBook(book);
                    }}>
                    {addGenres()}
                </select>
            </div>
        </div>
        <div className="create_books_row">
            <div className="create_books_item_area">
                <div className="create_books_item_title">Name</div>
                <input type="text" placeholder="Name" defaultValue={book.name} required
                    onChange={(e) => updateBook('name', e.target.value)}></input>
            </div>
            <div className="create_books_item_area">
                <div className="create_books_item_title">Author</div>
                <input type="text" placeholder="Author" defaultValue={book.author?.name} required
                    onChange={(e) => updateBook('authorName', e.target.value)}></input>
            </div>
            <div className="create_books_item_area">
                <div className="create_books_item_title">Price</div>
                <input type="number" placeholder = "Price" defaultValue={book.price} required
                    onChange={(e) => updateBook('price', e.target.value)}></input>
            </div>
            <div className="create_books_item_area">
                <button onClick={() => console.log('save')}>Save</button>
            </div>
        </div>
    </div>
}
function generateNewAuthor(name: string): Author {
    return {
        id: -1,
        name
    };
}
...
```

One important thing is I shouldn't set value by value like below.

```tsx
<select value={book.genreId.toString()} onChange={(e) => {}}></select>
```

Because the element's vlew won't be updated after changing the selected index.
So it looks like the same as default.

### Get URL Paramters
For getting "Book" data on page loading, I want to get URL parameters.

To do this, I can use "useParameters" of "react-router".

* [useParams - React Router: Declarative Routing for React.js](https://reactrouter.com/web/api/Hooks/useparams)

#### [Client] App.tsx
```tsx
import './App.css';
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";
import { SearchBooks } from './search/SearchBooks';
import { EditBooks } from './edit/EditBooks';

function App(): JSX.Element {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Search</Link>
            </li>
            <li>
              <Link to="/edit">Edit</Link>
            </li>
          </ul>
        </nav>
        <Switch>
          <Route path="/edit/:id?">
            <EditBooks />
          </Route>
          <Route path="/">
            <SearchBooks />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}
export default App;
```

#### [Client] EditBooks.tsx
```tsx
...
export function EditBooks(): JSX.Element {
...
    useEffect(() => {        
        bookGetter.getAllGenres()
            .then(genres => setGenres(genres));
        bookGetter.getExistedAuthors()
            .then(authors => setAuthors(authors));
        const id = getIntValue(params['id']);
        if(id != null) {
            bookGetter.getBookById(id)
                .then(book => 
                    (book?.id == null || book.id <= 0)? defaultBook: book)
                .then(book => setBook(book));
        }
    }, []);
...
```

#### [Server] index.ts
```ts
...
app.get('/book/:id', async (req, res) => {
  const id = parseInt(req.params.id);
  if(id == null || isNaN(id)) {
    res.json({});
    return;
  }
  const books = container.resolve(BookService);
  const result = await books.getBookById(id);
  if(result == null) {
    res.json({});
    return;
  }
  res.json(result);
});
...
```

#### bookService.ts
```ts
@autoInjectable()
export class BookService {
...
    public async getBookById(id: number): Promise<Book|null> {
        const connection = await this.context.getConnection();
        const result = await connection.getRepository(Book)
            .createQueryBuilder('book')
            .innerJoinAndSelect('book.genre', 'genre')
            .innerJoinAndSelect('book.author', 'author')
            .where('book.id = :id', { id })
            .getOne();
        return (result == null)? null: result;
    }
...
}
```

## Upload data
Because the uploading data are kept by state, I just need getting the data from the state and upload them.

#### [Client] EditBooks.tsx
```tsx
...
export function EditBooks(): JSX.Element {
...
    const upload = async () => {
        if(book.genreId == null ||
            book.genreId <= 0) {
            book.genreId = genres[0].id;
            book.genre = genres[0];
        } else {
            const genre = genres.find(g => g.id === book.genreId);
            if(genre == null) {
                book.genreId = genres[0].id;
                book.genre = genres[0];
            } else {
                book.genreId = genre.id;
                book.genre = genre;
            }
        }
        const result = await bookUploader.upload(book);
        if(result.succeeded === true) {
            alert('OK');
        } else {
            alert(result.errorMessage);
        }
    };
...
    return <div className="create_books_area">
...
            <div className="create_books_item_area">
                <button onClick={async () => await upload()}>Save</button>
            </div>
        </div>
    </div>
}
```
#### [Client] bookUploader.ts
```ts
import { ActionResult } from "../applications/application.type";
import { Book } from "../models/book";

export async function upload(book: Book): Promise<ActionResult> {
    return await fetch('http://localhost:3099/books', {
        method: 'POST',
        mode: 'cors',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(book),
    })
    .then(response => response.json())
    .then(json => {
        return JSON.parse(JSON.stringify(json));
    })
    .catch(err => {
        console.error(err);
        return {
            succeeded: false,
            errorMessage: err,
        };
    });
}
```

### CORS error?
As same as GET request, I wrote like this for treating POST request.

#### [Server] index.ts
```ts
...
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.post('/books', cors(corsOptionsDelegate), async (req, res) => {
  const book = JSON.parse(JSON.stringify(req.body));
  if(book == null) {
    res.json({
      succeeded: false,
      errorMessage: 'no targets'
    });
    return;
  }
  const books = container.resolve(BookService);
  res.json(await books.updateBook(book));
});
...
```

But I got a CORS error.

```
Access to fetch at 'http://localhost:3099/books' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
```

I don't know why. But after all, I change the code like below.

#### [Server] index.ts
```ts
...
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.use(cors(corsOptionsDelegate));
...
app.post('/books', async (req, res) => {
  const book = JSON.parse(JSON.stringify(req.body));
  if(book == null) {
    res.json({
      succeeded: false,
      errorMessage: 'no targets'
    });
    return;
  }
  const books = container.resolve(BookService);
  res.json(await books.updateBook(book));
});
...
```

* [cors - Express cors middleware](https://expressjs.com/en/resources/middleware/cors.html)

#### bookService.ts
```ts
...
@autoInjectable()
export class BookService {
    public async updateBook(book: Book): Promise<ActionResult> {        
        const connection = await this.context.getConnection();
        const queryRunner = connection.createQueryRunner();
        await queryRunner.startTransaction();
        try { 
            const author = await this.updateAuthor(connection, queryRunner, book.author);
            if(author == null) {
                return {
                    succeeded: false,
                    errorMesage: 'No authors'
                };
            }
            const genre = await this.getGenreById(connection, book.genreId);
            if(genre == null) {
                return {
                    succeeded: false,
                    errorMesage: 'Failed getting genre'
                };
            }
            let result: ActionResult;
            if(book.id <= 0) {
                result = await this.createBook(queryRunner, author, genre, book);
            } else {
                result = await this.updateExistedBook(connection, queryRunner, author, genre, book);
            }
            if(result.succeeded === true) {
                await queryRunner.commitTransaction();
            }
            return result;
        } catch (err) {
            console.error(err);
            await queryRunner.rollbackTransaction();
            return {
                succeeded: false,
                errorMesage: 'something wrong'
            };
        }
    }
...
    private async updateAuthor(connection: Connection, queryRunner: QueryRunner, author: Author): Promise<Author|null> {
        if(author?.name == null ||
                author.name.length <= 0) {
            return null;
        }
        const targetAuthor = await connection.getRepository(Author)
            .createQueryBuilder('author')
            .where('author.name = :name', { name: author.name })
            .getOne();
        if(targetAuthor != null) {
            return targetAuthor;
        }
        const newAuthor = Author.create(author);
        await queryRunner.manager.save(newAuthor);
        return newAuthor;
    }
    private async getGenreById(connection: Connection, id: number): Promise<Genre|undefined> {
        return await connection.getRepository(Genre)
            .createQueryBuilder('genre')
            .where('genre.id = :id', { id })
            .getOne();
    }
    private async createBook(queryRunner: QueryRunner, 
            author: Author, genre: Genre, book: Book): Promise<ActionResult> {
        const newBook = Book.create(book, author, genre);
        console.log(newBook);
        
        await queryRunner.manager.save(newBook);
        return {
            succeeded: true,
            errorMesage: ''
        };
    }
    private async updateExistedBook(connection: Connection, queryRunner: QueryRunner, 
            author: Author, genre: Genre, book: Book): Promise<ActionResult> {
        const existedBook = await connection.getRepository(Book)
            .createQueryBuilder('book')
            .where('book.id = :id', { id: book.id })
            .getOne();
        if(existedBook == null) {
            return {
                succeeded: false,
                errorMesage: 'target book was not found',
            };
        }
        existedBook.update(book, author, genre);
        await queryRunner.manager.save(existedBook);
        return {
            succeeded: true,
            errorMesage: ''
        };
    }
...
}
```