[TypeScript][Express] Try React 2
Masui Masanori
Posted on August 13, 2021
Intro
This time, I will try get and upload data to the Express application.
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
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
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
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
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.
Update value
For updating "Book" data by input data, I update states when "onChange" events are fired.
[Client] EditBooks.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.
<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".
[Client] App.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
...
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
...
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
@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
...
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
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
...
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
...
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));
});
...
bookService.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: ''
};
}
...
}
Posted on August 13, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.