Introducing PrrrStack, Pt. 2
In the first article of this series, we created a RestAPI for our application using Postgres, Rust, and Rocket. For the second half, we’ll be using React to create the front end. You can check out the project’s repo for the code.
Last week, we created some basic endpoints that allowed a user to create a new cat in our database, update an existing cat, retrieve all of our cats, or delete a cat. This week we’ll create the user interface that uses those endpoints.
To start off, make sure you have Node/NPM and Webpack installed and in your path.
You can put your SPA inside of your Rust application as it is in the repo or create it somewhere else entirely. Let’s go ahead and mkdir frontend && cd frontend
then npm init
. Here’s my package.json
:
{
“name”: “prrr_frontend”,
“version”: “0.1.0”,
“description”: “a react frontend for the prrrstack demo”,
“main”: “index.js”,
“scripts”: {
“test”: “echo \”Error: no test specified\” && exit 1",
“dev”: “webpack-dev-server — mode development”,
“build”: “webpack — mode production”
},
“keywords”: [
“prrr”
],
“author”: “crash springfield”,
“license”: “MIT”,
“dependencies”: {
“axios”: “^0.18.0”,
“hoek”: “^5.0.3”,
“react”: “^15.4.0”,
“react-dom”: “^16.4.0”
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-core": "^6.26.3",
"babel-loader": "^7.1.4",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"css-loader": "^0.28.11",
"node-sass": "^4.9.0",
"sass-loader": "^7.0.3",
"style-loader": "^0.21.0",
"webpack": "^4.12.0",
"webpack-cli": "^2.0.14",
"webpack-dev-server": "^3.1.4"
}
}
We’re not using hoek
directly, but some of our dependencies require a version with security vulnerabilities, so we’re specifying a newer version. We’ve also got quite a few development dependencies — some necessary for compiling React and others fine-tuned to my coding style.
On the subject of building, let’s create our .babelrc
:
{
"presets": [
"es2015",
"stage-2",
"react",
],
"plugins": [
"transform-class-properties"
]
}
and our webpack.config.js
const path = require('path')
const webpack = require('webpack')
const bundlePath = path.resolve(__dirname, 'dist/')module.exports = {
entry: "./src/index.js",
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
plugins: [
'transform-class-properties'
],
presets: [
'es2015',
'react',
'stage-2'
]
}
},
{
test: /\.scss$/,
loaders: ['style-loader', 'css-loader', 'sass-loader']
},
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
}
]
},
resolve: { extensions: ['*', '.js', '.jsx'] },
output: {
publicPath: bundlePath,
filename: "bundle.js"
},
devServer: {
contentBase: path.join(__dirname,'public'),
port: 3000,
publicPath: "http://localhost:3000/dist"
},
plugins: [ new webpack.HotModuleReplacementPlugin() ]
}
Basically, we’re setting it up to compile our React/Sass code down to JavaScript. Our presets let us use ECMAScript 2015, features from 2016/2017, and JSX. It’s possible to build React applications without JSX, but I like the simplicity it provides.
We’re also running our front end off a development server with hot reloading, so every time we make a change, it will compile for us.
We’re almost done with the boilerplate, so let’s go ahead and set up our directory structure and test to make sure everything works.
/frontend
|-----.babelrc
|-----package.json
|-----webpack.config.js
|-----/public/index.html
|-----/src
|---/components
| |----Cat.js
| |----Cat.scs
| |----EditCat.js
| |----EditCat.scss
|
|---/utils/api.js
|---App.js
|---App.scss
|---index.js
And we’ll set up our index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Prrr Demo</title>
</head>
<body>
<div id="root"></div>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<script src="../dist/bundle.js"></script>
</body>
</html>
And the basics of our React app in index.js
:
import React from 'react'
import ReactDom from 'react-dom'const Hello = () =>
<div>
Hello world
</div>ReactDom.render(
<Hello />,
document.getElementById('root')
)
If we run npm run dev
we should see our app at localhost:3000. Pretty boring though.
Now that our app is up and running, we can start building the interesting stuff. Before fully diving into the React-specific pieces though, let’s get our utils/api.js
working so that we’ll have data to display.
import axios from 'axios'const base_url = `http://localhost:8000/api/cats`export default {getAllCats: () => axios.get(`${base_url}`)
.then(res => {
if (res.status === 200) {
return res.data.result
}
throw new Error(res.error)
}),addCat: (cat) => axios.post(`${base_url}`, cat)
.then(res => {
if (res.status === 200 || res.status === 201) {
return res.data.result
}
throw new Error(res.error)
}),updateCat: (cat, cat_id) => axios.put(`${base_url}/${cat_id}`, cat)
.then(res => {
if (res.status === 200 || res.status === 201) {
return res.data.result
}
throw new Error(res.error)
}),deleteCat: cat_id => axios.delete(`${base_url}/${cat_id}`)
.then(res => {
if (res.status === 200 || res.status === 204) {
return res.data.result
}
throw new Error(res.error)
})}
It’s fairly simple. We’re using axios
to make our API requests and then returning the response’s data’s results array, which will be our list of cats. One thing to notice is the status code on some of the methods — we’re not just checking for the expected response (201/204) but also allowing for a 200 response. This is because the initial response returned is to the HEADERS or OPTIONS request happening behind the scenes.
Next let’s set up our App.js
and see if we can load the data into it.
import React, { Component } from 'react'import api from './utils/api'
import './App.scss'export default class App extends Component {
constructor(props) {
super(props)
this.state = {
cats: []
}
}componentDidMount = () => {
api.getAllCats()
.then(cats => {
console.log(cats)
this.setState({ cats: cats }
)})
}render = () =>
<div className="page">
<h1>PrrrStack Cat Demo</h1>
<div className="container">
</div>
</div>
}
and update our index.js
to include our app:
import React from 'react'
import ReactDom from 'react-dom'import App from './App.js'ReactDom.render(
<App />,
document.getElementById('root')
)
Make sure you’ve got your RestAPI up and running with cargo run
and then restart your front end with npm run devserver
if it’s not already running. Now, if you look in the console, you should see your cats array:
[
{
bio: "the world's greatest cat",
id: 1,
image_url: "http://res.cloudinary.com/dotkbdwdw/image/upload/v1489768456/dss9ufrspwkpaaz7h1iu.jpg",
kills: 57,
name: "Fredy"
}
}
Unless you’re in Firefox and are met with Cross-Origin Request Blocked
and sadness and no cats. But even if you have your cats, you can’t see them. What good is that? Well, let’s fix it with a Cats.js
component:
import React from 'react'
import './Cat.scss'const Cat = props =>
<div className="cat">
<h2 className="cat-name">{ props.name }</h2>
<div className="image-container">
<img src={ props.image_url } />
</div>
<p className="cat-bio">{ props.bio }</p>
<div className="kills">
<span className="kills-label">Kill count: </span>
<span className="kill-count">{ props.kills }</span>
</div>
<div className="buttons">
<button className="edit"
onClick={ props.edit }>Edit</button>
<button className="delete"
onClick={ props.delete }>Delete</button>
</div>
</div>export default Cat
Here, we’re using a stateless functional component because there is no reason for this piece to change once it is rendered. You can think of as a pure function that takes in a cat object and returns a rendered HTML element. Here’s the styles in Cats.scss
:
.cat {
margin: 20px;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.16), 0 2px 5px 0 rgba(0, 0, 0, 0.26); .cat-name {
margin-top: 0;
} .image-container {
width: 450px; img {
max-width: 100%;
max-height: 100%;
}
} .cat-bio {} .kills { .kills-label {
font-weight: bold;
}
.kill-count {}
} .buttons {
margin-top: 10px; .edit {
margin-right: 20px;
padding: 0;
height: 30px;
width: 60px;
border-width: 0;
outline: none;
border-radius: 2px;
box-shadow: 0 1px 4px rgba(0, 0, 0, .6);
background-color: #2ecc71;
color: #ecf0f1;
transition: background-color .3s; &:hover, &:focus {
background-color: #27ae60;
}
} .delete {
margin-right: 20px;
padding: 0;
height: 30px;
width: 60px;
border-width: 0;
outline: none;
border-radius: 2px;
box-shadow: 0 1px 4px rgba(0, 0, 0, .6);
background-color: #e74c3c;
color: #ecf0f1;
transition: background-color .3s; &:hover, &:focus {
background-color: #c0392b;
}
}
}
}
There’s a lot to be added but it’s a start. Finally, if we want to see our kitties we have to import them into our App.js
with import Cat from './components/Cat'
and create a render method that maps over the cats array in our state, returning a Cat
for each one:
renderCats = () => this.state.cats.map(cat =>
<Cat key={cat.id}
name={cat.name}
image_url={cat.image_url}
bio={cat.bio}
kills={cat.kills}
edit={e => this.editCat(cat)}
delete={e => this.deleteCat(cat)}
/>
)
Note here the additional key
property to make React happy and keep elements straight. We’ll get to edit
and delete
later.
Last, let’s update our render()
method to call our renderCats()
function:
render = () =>
<div className="page">
<h1>PrrrStack Cat Demo</h1>
<div className="container">
{ this.renderCats() }
</div>
</div>
Holy meow it works! By setting the result of our API request in state and rendering from that state we’re able to see the cats. Furthermore, because we initialized an empty array called cats
in our state, if our API request fails or returns nothing, our app doesn’t crash.
We have one CRUD method working and a few buttons that throw errors when you try clicking them, so let’s go ahead and take care of that. The delete method that we’re passing to our cat component is simple enough.
deleteCat = (cat) => {
api.deleteCat(cat.id)
.then(res => {
const cats = this.state.cats.filter(c => c.id != cat.id)
this.setState({ cats: cats })
})
}
Now, clicking the delete button in the Cat component calls the function passed into it as delete
, which we can see in our renderCats()
method is our App component’s deleteCat()
. This calls our API’s method of the same name, and then instead of doing anything with the response, we’re being lazy and just filtering out our deleted cat from state.
The edit method is going to be a little more complicated out of DRY laziness. I want to edit cats in a modal, but I also want to use a modal to create new cats, so let’s take a detour and return to it at the end. First, let’s style up a modal in App.scss
.
.container {
display: flex; .add {
margin-right: 20px;
padding: 0 20px;
height: 30px;
border-width: 0;
outline: none;
border-radius: 2px;
box-shadow: 0 1px 4px rgba(0, 0, 0, .6);
background-color: #2ecc71;
color: #ecf0f1;
transition: background-color .3s; &:hover, &:focus {
background-color: #27ae60;
}
}
}.modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.4);.modal-content {
background-color: #fff;
margin: 15% auto;
padding: 20px;
width: 40%;
border-radius: 10px;
}
}.show {
display: block;
}
Notice that show
class on the bottom? That’s for our modal. We’re going to also going to sidestep React for the functionality just a little by adding
window.onclick = event => {
if (event.target === modal) {
modal.classList.toggle('show')
}
}
in our App.js
but outside of the component itself. Popups are awful and it’s much easier to just click outside of them to make them go away rather than find the x
button or whatever.
Next, we’ll set up our EditCat.js
component, which we’ll use both for editing existing cats and creating new ones.
import React from 'react'
import './EditCat.scss'const EditCat = props =>
<div className="edit-cat">
<div>
<input
className="input"
type="text"
defaultValue={ props.name }
onChange={ props.updateName } />
<label className="label">Name: </label>
</div>
<div>
<input
className="input"
type="text"
defaultValue={ props.image_url }
onChange={ props.updateImage } />
<label className="label">Image Url: </label>
</div>
<div>
<textarea
rows="2"
className="textarea"
defaultValue={ props.bio }
onChange={ props.updateBio } />
<label className="label">Bio: </label>
</div>
<div>
<input
className="input"
type="text"
defaultValue={ props.kills }
onChange={ props.updateKills } />
<label className="label">Kill Count: </label>
</div>
<div className="buttons">
<button className="edit"
onClick={ props.saveEdits }>Save Edits</button>
</div>
</div>export default EditCat
Again, we’re using a stateless functional component. Here, each input is pre-populated with the value passed down to it, which will be the current cat’s stats, or empty if we’re making a new cat. Each input is also echoing up its current value with the updateWhatever()
function that’s being passed down to it.
Seeing how we render EditCat.js
inside App.js
makes this clearer. (Don’t forget to import it import EditCat from './components/EditCat'
)
renderEditModal = () =>
<EditCat
name={this.state.editingCat.name}
image_url={this.state.editingCat.image_url}
bio={this.state.editingCat.bio}
kills={this.state.editingCat.kills}
updateName={this.updateName}
updateImage={this.updateImage}
updateBio={this.updateBio}
updateKills={this.updateKills}
saveEdits={
this.state.isNew ? this.saveNewCat : this.saveEdits
} />
The cat stats are being passed down from something in our state called editingCat
that we haven’t created yet but will shortly. There are also a bunch of edit methods we’ve yet to write. Finally, there’s a ternary expression that checks the state to see whether the EditCat in the modal is new or old. If it’s new, we’re calling a saveNewCat()
method; if not, we call saveEdits()
.
Let’s first update our render method:
render = () =>
<div className="page">
<h1>PrrrStack Cat Demo</h1>
<div className="container">
<button className="add"
onClick={this.addNewCat}>Add another cat</button>
</div>
<div className="container">
{ this.renderCats() }
<div id="modal" className="modal">
<div className="modal-content">
{ this.state.editingCat ? this.renderEditModal() : '' }
</div>
</div>
</div>
</div>
So if our state says we’re editing a cat, call the renderEditModal()
function. Otherwise, display nothing. We also have a new button for adding a new cat. Finally, let’s update our state to include everything we’ll need:
this.state = {
cats: [],
editingCat: null,
isNew: false,
}
That should guarantee that our app doesn’t break when it renders, but we still have to set up the methods before it’s working.
We’ll be opening the modal when we edit a cat or create a new one, and we’ll also want to close after saving edits, so let’s abstract that out into its own function:
toggleModal = () => {
const modal = document.getElementById('modal')
modal.classList.toggle('show')
}
Now we can set the modal up for adding a new cat. First, let’s create the addNewCat()
method referenced in our render()
function:
addNewCat = () => {
const cat = {
name: "",
image_url: "",
bio: "",
kills: ""
}
this.setState({ editingCat: cat, isNew: true })
this.toggleModal()
}
Here, were creating an empty cat object with the fields we can create, then setting it to the edittingCat
in our state. We’re also setting isNew
to true
because our EditCat needs to know whether we’re creating a new cat or editing an old one when we click save.
The modal now opens, but what happens when we type something? Well, nothing because we haven’t created our update methods, so let’s do that next.
updateName = e => {
const cat = { ...this.state.editingCat, name: e.target.value }
this.setState({ editingCat: cat})
}updateImage = e => {
const cat = {
...this.state.editingCat,
image_url: e.target.value
}
this.setState({ editingCat: cat})
}updateBio = e => {
const cat = { ...this.state.editingCat, bio: e.target.value }
this.setState({ editingCat: cat})
}updateKills = e => {
const cat = {
...this.state.editingCat,
kills: parseInt(e.target.value)
}
this.setState({ editingCat: cat})
}
These are fairly self-explanatory. We listen to the input’s event passed up from EditCat into App, make a copy of the current state’s editingCat
, and then set the state with our new cat object. Kills has a parseInt()
thrown around it to make sure we get an Integer because passing something else will break Rust’s type checking, but it’s also very sloppy and not the way you should handle this in production.
We still can’t save the cat, so let’s create a method for that:
saveNewCat = () => {
const cat = {
name: this.state.editingCat.name,
image_url: this.state.editingCat.image_url,
bio: this.state.editingCat.bio,
kills: this.state.editingCat.kills
}
api.addCat(cat)
.then(res => {
this.setState({ cats: res})
this.toggleModal()
})
}
Here, we take the cat from the state and create an object (notice the structure is the same as our NewCat struct in Rust). We then pass that cat to our API and reset our state to be the new cats array we get back in the response. After that, the modal closes.
Let’s try it with Nardwuar:
Now, all we have left to do is save edits we make to existing cats, and since we’re re-using so much code, we have to add only two more functions.
First is the editCat()
method in our App component that the edit button on our Cat component calls:
editCat = cat => {
this.setState({ editingCat: cat, isNew: false })
this.toggleModal()
}
Here, it’s referencing the cat mapped in our renderCats()
method
At the end ofrenderCats()
we were passing props to our Cat’s edit button like so:
edit={e => this.editCat(cat)}
The cat
isn’t being propogated up from the Cat component (like the event trigger is) but is just referencing the cat currently being mapped over. We pass this cat into our state’s editingCat
we also used for creating a new cat. This time, however, isNew
is set to false
so the ternary
saveEdits={this.state.isNew ? this.saveNewCat : this.saveEdits}
in our EditCat component will call the saveEdits()
method instead. Let’s create that now.
saveEdits = () => {
const cat = {
name: this.state.editingCat.name,
image_url: this.state.editingCat.image_url,
bio: this.state.editingCat.bio,
kills: this.state.editingCat.kills
}
api.updateCat(cat, this.state.editingCat.id)
.then(res => {
this.setState({
editingCat: null,
cats: res,
editingCat: false
})
this.toggleModal()
})
}
As you can see, it’s pretty similar to saveNewCat()
in that we’re using the editingCat
from our state, but the key difference is we’re also passing the id
to the API call because the update
method needs to know which cat it’s replacing.
And there you have it: a quick and purdy introduction to PrrrStack. Here’s another link to the project’s repo if you can’t get something to work and don’t want to go looking for it.