Step 7

Creating a book

Let's add a page to create a book

First, let's create a new type for creating a book

src/types/index.d.ts

export interface ICreateBook {
	title: string;
	author: string;
}

export interface IBook extends ICreateBook {
	id: number;
	createdAt: string;
	updatedAt: string;
}

Here, we just split up our book interface into 2 interfaces, where IBook is an extension of ICreateBook.

Next, we create a few more actions, which we will use in later steps

src/actions/index.ts

import axios from 'axios';
import { CONFIG } from '../config';
import { IBook, ICreateBook } from '../types';
const { SERVER_URL } = CONFIG;

const api = axios.create({
	baseURL: SERVER_URL
});

export const fetchBooks = async (): Promise<IBook[]> => {
	try {
		const { data } = await api.get('/api/books');
		return data.response;
	} catch (error) {
		throw error.response.data.error;
	}
};

export const fetchBook = async (id: number | string): Promise<IBook> => {
	try {
		const { data } = await api.get(`/api/books/${id}`);
		return data.response;
	} catch (error) {
		throw error.response.data.error;
	}
};

export const deleteBook = async (id: number | string): Promise<IBook> => {
	try {
		const { data } = await api.delete(`/api/books/${id}`);
		return data.response;
	} catch (error) {
		throw error.response.data.error;
	}
};

export const createBook = async (body: ICreateBook): Promise<IBook> => {
	try {
		const { data } = await api.post(`/api/books/`, body);
		return data.response;
	} catch (error) {
		throw error.response.data.error;
	}
};

export const updateBook = async (
	id: number | string,
	body: ICreateBook
): Promise<IBook> => {
	try {
		const { data } = await api.put(`/api/books/${id}`, body);
		return data.response;
	} catch (error) {
		throw error.response.data.error;
	}
};

Now, let's create our CreateBook page

src/containers/CreateBook.tsx

import React, { Component, ChangeEvent, FormEvent } from 'react';
import { createBook } from '../actions';
import { RouteComponentProps } from 'react-router-dom';
import BookForm from '../components/BookForm';

export default class CreateBookPage extends Component<RouteComponentProps> {
	state = {
		title: '',
		author: ''
	};

	onChange = (e: ChangeEvent<HTMLInputElement>) =>
		this.setState({ [e.target.name]: e.target.value });

	onSubmit = async (e: FormEvent<HTMLFormElement>) => {
		e.preventDefault();
		const { title, author } = this.state;
		if (!title || !author) return;

		await createBook(this.state);
		this.props.history.push('/books');
	};

	render() {
		return (
			<div>
				Create a book:
				<BookForm
					{...this.state}
					onSubmit={this.onSubmit}
					onChange={this.onChange}
				/>
			</div>
		);
	}
}

We create our state in our container component and pass references to them down to our BookForm as props. When we submit the form, the BookForm will run the onSubmit function in our CreateBookPage component, where we create the book in our backend and then redirect to the books page. For our BookForm:

src/components/BookForm.tsx

import React, { FormEvent, ChangeEvent } from 'react';

type Props = {
	onSubmit: (e: FormEvent<HTMLFormElement>) => Promise<void>;
	onChange: (e: ChangeEvent<HTMLInputElement>) => void;
	title: string;
	author: string;
};

export default function BookForm({ onSubmit, onChange, title, author }: Props) {
	return (
		<form onSubmit={onSubmit}>
			<label>
				Title: <input name="title" value={title} onChange={onChange} />
			</label>
			<br />
			<label>
				Author: <input name="author" value={author} onChange={onChange} />
			</label>
			<br />
			<input type="submit" value="Submit" />
		</form>
	);
}

Now we add the page to our App

src/App.tsx

import React, { Component } from 'react';
import './App.css';
import BooksPage from './containers/Books';
import { Switch, Route } from 'react-router-dom';
import HomePage from './containers/Home';
import Navbar from './components/Navbar';
import BookPage from './containers/Book';
import CreateBookPage from './containers/CreateBook';

class App extends Component {
	render() {
		return (
			<div className="App App-header">
				<Navbar />
				<br />
				<Switch>
					<Route exact path="/" component={HomePage} />
					<Route exact path="/books/create" component={CreateBookPage} />
					<Route exact path="/books/:id" component={BookPage} />
					<Route exact path="/books" component={BooksPage} />
				</Switch>
			</div>
		);
	}
}

export default App;

Notice that the route for creating a book is above the book page. This is because the URL paths are matched in order, and since the create book page path is a match for the book page path, if it were above the create book route, then we would never be able to navigate to our create book page.

Finally, we will add a link to our new page in our navbar

src/components/Navbar.tsx

import React, { Component } from 'react';
import { Link } from 'react-router-dom';

export default class Navbar extends Component {
	render() {
		return (
			<nav>
				<Link to="/" className="App-link">
					Home
				</Link>{' '}
				|{' '}
				<Link to="/books" className="App-link">
					Books
				</Link>{' '}
				|{' '}
				<Link to="/books/create" className="App-link">
					Create Book
				</Link>
			</nav>
		);
	}
}

Last updated

Was this helpful?