- Fork
- Clone
We've built the backend. Now it's time to build the frontend.
In this project we are going to be using react to build a CRUD frontend to our items json api.
cd express-api-react
npm install
Checkout the database config:
config/config.json
{
"development": {
"database": "items_app_development",
"dialect": "postgres"
},
"test": {
"database": "items_app_test",
"dialect": "postgres"
},
"production": {
"use_env_variable": "DATABASE_URL",
"dialect": "postgres",
"dialectOptions": {
"ssl": true
}
}
}
Now run $ npm run db:reset
to set up your database.
Make sure the data exists:
psql items_app_development
SELECT * FROM "Items";
Run the server:
npm start
Test the following routes in your browser:
Now open a new tab in the terminal. Make sure you're inside the repo.
Let's create our React app.
npx create-react-app client
Let's start by adding react router:
cd client
npm install react-router-dom
Important: Notice how there are two package.json's one in the root of the repo for the server, and the other inside the client folder. Make sure you're inside the client folder. We want to install the react router package so we can use it for the react app.
And now let's setup our app to use react router:
client/src/index.js
import { BrowserRouter as Router } from "react-router-dom";
ReactDOM.render(
<Router>
<App />
</Router>,
document.getElementById("root")
);
Cool. Now let's setup our routes. A route will render an associated component. Below is the list:
/
- the homepage, just display a welcome screen. It will render a Home component.
/items
- the ability to see all items. It will render an Items component.
/create-item
- the ability to create a new item. It will render an ItemCreate component.
/items/:id
- the ability to see a specific item. It will render an Item component.
/items/:id/edit
- the ability to edit an item. It will render an ItemEdit component.
Let's start by creating our empty components in client/src directory:
mkdir components
cd components
mkdir routes
cd routes
touch Home.jsx Item.jsx ItemCreate.jsx ItemEdit.jsx Items.jsx
Now let's create our routes:
client/App.js
import React from 'react'
import { Route } from 'react-router-dom'
import Items from './components/routes/Items'
import Item from './components/routes/Item'
import ItemEdit from './components/routes/ItemEdit'
import ItemCreate from './components/routes/ItemCreate'
import Home from './components/routes/Home'
const App = () => (
<React.Fragment>
<Route exact path='/' component={Home} />
<Route exact path='/items' component={Items} />
<Route exact path='/create-item' component={ItemCreate} />
<Route exact path='/items/:id' component={Item} />
<Route exact path='/items/:id/edit' component={ItemEdit} />
</React.Fragment>
)
export default App
A simple Home component: src/components/routes/Home.jsx
import React from 'react'
import Layout from '../shared/Layout'
const Home = () => (
<Layout>
<h4>Welcome to the items app!</h4>
</Layout>
)
export default Home
Notice the Layout component. We are going to build the Layout component next. This is a shared component that we will re-use multiple times. Essentially, the Layout component is the shell of the web app we are building.
Let's create our "shared" components. The idea of shared components is that anytime we have code that we would repeat in several components (a footer, a navbar, etc), we can wrap that code inside a component and import it in whenever needed.
cd client/src/components
mkdir shared
cd shared
touch Layout.jsx Footer.jsx Nav.jsx
Let's start with the Layout component:
components/shared/Layout.jsx
import React from 'react'
import Nav from './Nav'
import Footer from './Footer'
const Layout = props => (
<div>
<h1>Items App</h1>
<Nav />
{props.children}
<Footer />
</div>
)
export default Layout
Note: We are using
props.children
here. React Children is a placeholder for which ever component calls the component thatprops.children
is in. You will see this in action in a minute.
Let's create our Nav component:
components/shared/Nav.jsx
import React from 'react'
import { NavLink } from 'react-router-dom'
const Nav = () => (
<nav>
<NavLink to='/'>Home</NavLink>
<NavLink to='/items'>Items</NavLink>
<NavLink to='/create-item'>Create Item</NavLink>
</nav>
)
export default Nav
And the Footer component:
components/shared/Footer.jsx
import React from 'react'
const Footer = () => (
<p>© Copyright {new Date().getFullYear()}. All Rights Reserved.</p>
)
export default Footer
Let's make sure the app is working.
cd express-api-react
npm start
Open a new tab in your terminal and run your client:
cd client
npm start
Open your browser and test the route http://localhost:3001/. The Home component should render but the other links will not work yet because we haven't built them out yet.
Cool. We are done with shared components for now.
Now let's build the Items component.
We will be making an axios call in the Items component to fetch all the Items from the server.
Let's start by installing axios. Make sure you're in the client folder.
cd client
npm install axios
When you run
npm install axios
, make sure you're inside the client folder where the package.json exists.
Now we can build the Items component:
import React, { Component } from 'react'
import { Link } from 'react-router-dom'
import axios from 'axios'
class Items extends Component {
constructor (props) {
super(props)
this.state = {
items: []
}
}
async componentDidMount () {
try {
const response = await axios(`http://localhost:3001/api/items`)
this.setState({ items: response.data.items })
} catch (err) {
console.error(err)
}
}
render () {
const items = this.state.items.map(item => (
<li key={item.id}>
<Link to={`/items/${item.id}`}>{item.title}</Link>
</li>
))
return (
<>
<h4>Items</h4>
<ul>
{items}
</ul>
</>
)
}
}
export default Items
Test the http://localhost:3001/api/items route in your browser.
Good? Great. Let's move on to the Item component.
components/routes/Item.jsx
import React, { Component } from 'react'
import { Link, Redirect } from 'react-router-dom'
import axios from 'axios'
import Layout from '../shared/Layout'
class Item extends Component {
constructor(props) {
super(props)
this.state = {
item: null,
deleted: false
}
}
async componentDidMount() {
try {
const response = await axios(`http://localhost:3001/api/items/${this.props.match.params.id}`)
this.setState({ item: response.data.item })
} catch (err) {
console.error(err)
}
}
destroy = () => {
axios({
url: `http://localhost:3001/api/items/${this.props.match.params.id}`,
method: 'DELETE'
})
.then(() => this.setState({ deleted: true }))
.catch(console.error)
}
render() {
const { item, deleted } = this.state
if (!item) {
return <p>Loading...</p>
}
if (deleted) {
return <Redirect to={
{ pathname: '/', state: { msg: 'Item succesfully deleted!' } }
} />
}
return (
<Layout>
<h4>{item.title}</h4>
<p>Link: {item.link}</p>
<button onClick={this.destroy}>Delete Item</button>
<Link to={`/items/${this.props.match.params.id}/edit`}>
<button>Edit</button>
</Link>
<Link to="/items">Back to all items</Link>
</Layout>
)
}
}
export default Item
We should now be able to see http://localhost:3001/items/1.
Next, we want to implement the ItemEdit and ItemCreate. Inside the ItemEdit component we will have a form to edit an item. And Inside the ItemCreate component we will have form to create an item. What if we could abstract those two forms into one? We can, so let's do that now by creating another shared component called ItemForm:
cd components/shared/
touch ItemForm.jsx
components/shared/ItemForm.jsx
import React from 'react'
import { Link } from 'react-router-dom'
const ItemForm = ({ item, handleSubmit, handleChange, cancelPath }) => (
<form onSubmit={handleSubmit}>
<label>Title</label>
<input
placeholder="A vetted item."
value={item.title}
name="title"
onChange={handleChange}
/>
<label>Link</label>
<input
placeholder="http://acoolitem.com"
value={item.link}
name="link"
onChange={handleChange}
/>
<button type="submit">Submit</button>
<Link to={cancelPath}>
<button>Cancel</button>
</Link>
</form>
)
export default ItemForm
Awesome! Now let's build our ItemEdit component:
components/routes/ItemEdit.jsx
import React, { Component } from 'react'
import { Redirect } from 'react-router-dom'
import axios from 'axios'
import ItemForm from '../shared/ItemForm'
import Layout from '../shared/Layout'
class ItemEdit extends Component {
constructor(props) {
super(props)
this.state = {
item: {
title: '',
link: ''
},
updated: false
}
}
async componentDidMount() {
try {
const response = await axios(`http://localhost:3001/api/items/${this.props.match.params.id}`)
this.setState({ item: response.data.item })
} catch (err) {
console.error(err)
}
}
handleChange = event => {
const updatedField = { [event.target.name]: event.target.value }
const editedItem = Object.assign(this.state.item, updatedField)
this.setState({ item: editedItem })
}
handleSubmit = event => {
event.preventDefault()
axios({
url: `http://localhost:3001/api/items/${this.props.match.params.id}`,
method: 'PUT',
data: { item: this.state.item }
})
.then(() => this.setState({ updated: true }))
.catch(console.error)
}
render() {
const { item, updated } = this.state
const { handleChange, handleSubmit } = this
if (updated) {
return <Redirect to={`/items/${this.props.match.params.id}`} />
}
return (
<Layout>
<ItemForm
item={item}
handleChange={handleChange}
handleSubmit={handleSubmit}
cancelPath={`/items/${this.props.match.params.id}`}
/>
</Layout>
)
}
}
export default ItemEdit
Let's test that. Open http://localhost:3001/items/1 and edit a field.
Nice! Now try delete. Bye.
Ok. We have one last CRUD action to complete in our react app - CREATE. Let's build the ItemCreat component and use our ItemForm shared component:
components/routes/ItemForm.jsx
import React, { Component } from 'react'
import { Redirect } from 'react-router-dom'
import axios from 'axios'
import apiUrl from '../../apiConfig'
import ItemForm from '../shared/ItemForm'
import Layout from '../shared/Layout'
class ItemCreate extends Component {
constructor(props) {
super(props)
this.state = {
item: {
title: '',
link: ''
},
createdItem: null
}
}
handleChange = event => {
const updatedField = { [event.target.name]: event.target.value }
const editedItem = Object.assign(this.state.item, updatedField)
this.setState({ item: editedItem })
}
handleSubmit = event => {
event.preventDefault()
axios({
url: `${apiUrl}/items`,
method: 'POST',
data: { item: this.state.item }
})
.then(res => this.setState({ createdItem: res.data.item }))
.catch(console.error)
}
render() {
const { handleChange, handleSubmit } = this
const { createdItem, item } = this.state
if (createdItem) {
return <Redirect to={`/items`} />
}
return (
<Layout>
<ItemForm
item={item}
handleChange={handleChange}
handleSubmit={handleSubmit}
cancelPath="/"
/>
</Layout>
)
}
}
export default ItemCreate
Great, test the create in your browser.
We now have full CRUD complete on the backend and on the frontend.
Success.
Notice how we using the api url in multiple components. What would happen if we need to update the url, we would have to change it in several places. What if we could store the api url in one place and have it accessed from there? That way if we want to change the api url, we only change it in one place. We can do this!
Let's create an apiConfig component:
src/
touch apiConfig.jsx
src/apiConfig.jsx
let apiUrl
const apiUrls = {
production: 'https://sei-items-api.herokuapp.com/api',
development: 'http://localhost:3001/api'
}
if (window.location.hostname === 'localhost') {
apiUrl = apiUrls.development
} else {
apiUrl = apiUrls.production
}
export default apiUrl
Now replace all instances of http://localhost:3000/api in you Items, Item, ItemCreate, and ItemEdit components with ${apiUrl}
and don't forget to import the apiConfig component: import apiUrl from '../../apiConfig'
Deploying to Heroku and Surge
heroku create your-heroku-app-name
heroku buildpacks:set heroku/nodejs
heroku addons:create heroku-postgresql:hobby-dev --app=your-heroku-app-name
git status
git commit -am "add any pending changes"
git push heroku master
heroku run npx sequelize-cli db:migrate
heroku run npx sequelize-cli db:seed:all
Having issues? Debug with the Heroku command
heroku logs --tail
to see what's happening on the Heroku server.
Test the endpoints :)
Cool the backend is now deployed! On to the frontend:
First you will have to replace anywhere inside your react app where you made an axios call to localhost:3000 to https://your-heroku-app-name.herokuapp.com - if you completed the bonus that means you will only have to update the apiConfig.js file with https://your-heroku-app-name.herokuapp.com as the value for the production key.
Now let's deploy the frontend:
cd client
npm run build
cd build
mv index.html 200.html
npx surge
Follow the prompts on Surge. Test the frontend routes once deployed. Getting errors? Check the browser dev tools. Check
heroku logs --tail
Congrats! You built a full crud app with a backend and a frontend. You are now a fullstack developer!
✊ Fist to Five
Take a minute to give us feedback on this lesson so we can improve it!