Intro
This is Part I of a eight part series on building a CRUD application with React + Redux. You can view the code for this project here. You can view the table of contents here
React for Embereños
At the Flatiron school, we've been gearing up to teach students to build React applications--building out new curriculum content and preparing projects and assignments.
As I dove in to learning React, I struggled to put all the moving pieces together, trace the flow of data through my application, and understand the larger architecture. I think my Ember experience spoiled me a bit by abstracting away pretty much all of the boiler plate, and offering command line generators for the rest. Coming from Ember's MVC-like structure to the action creators, stores, reducers and components of React + Redux was a challenge.
That being said, the more I work with React, the more I can see where is really shines.
We all know that React is fast. It uses the virtual DOM to track the state of the actual DOM, only re-rendering discrete sections of the DOM as changes to application state dictate.
But where I really started to see the advantages of React was in the flow of data through the application, in the purely Data Down Actions Up system of maintaining application state.
In React, your components can have both state and properties. A component's properties should be considered immutable. Instead of a component responding to a user's action by updating it's own properties, it will send an action that updates the entire application's state, which in turn triggers a re-render of a component, updating that component's properties as a result.
To my mind, Ember somewhat obscures this DDAU flow, and it never really felt cogent to me until manually implementing it in React.
So, now that we're more or less convinced of the efficiency and elegance of React, let's build a thing!
Table of Contents
This post will take us through an entire React + Redux CRUD application, from set-up to all of our CRUD features. So, I've split it up into the following parts:
I. Application Architecture and Dependencies
II. React Router and Container Components
III. Async Redux: Connecting to an External API
IV. Index Feature
V. Show Feature
VI. Edit Feature
VII. Create Feature
VIII. Delete Feature
Part I: Application Architecture and Dependencies
Application Background
In order to gain a better understanding of building in React + Redux, I set out to build a super simple CRUD application, backed by a Rails API. In the interests of laziness, I decided to use an API that I built out for a previous blog post, with a few minor changes. You can check out the API code here.
Our app, Cat Book, allows a user to browse, create, edit and delete cats. Cat's have a few attributes like name, breed, weight and temperament. A cat also has many hobbies, like eating, playing, etc.
The Rails API
Our Rails API has two models, Cat and Hobby. A cat has many hobbies and a hobby can belong to many cats, so a join table was implemented to enact that relationship.
It's important to note that our Rails 5 API is using Active Model Serializer with the default adapter. JSON API serialization format doesn't really play nicely with React.
Our API doesn't have any authentication yet, though I'm planning to add that feature and discuss it in a future blog post.
Application Design
Let's take a look at the different pieces of our app and map out the components that we'll need to build.
Our app will have a header component, a cats index component, and a cat show component, to start with.
All of our components, as per React Redux convention, will be contained within a parent, or Application, component. The Application component will contain all of our other components, some of which will be rendered as its children.
Let's take a look:
We can see that the application container, represented in orange, wraps or contains all of the other components. The header component is rendered within the application container. The cats index is rendered within the application container.
The cats show page, however, is wrapped in both blue and orange to represent that it is the child of the cats index component, which is in turn a child of the container component.
As our application grows, we'll create a few additional components for creating and editing a cat, both of which will exist on the same level as the cats show component--as children of cats index.
So, we can see that React encourages us to build a hierarchy of components. As we build out our components well see that we can use this hierarchy to manage applications state. Some components that act as parents will maintain their own internal state, and pass aspects of this state down to their children components as props.
Now that we have a basic sense of the design of our app, let's start hooking up our initial routes and components.
But first, we'll set up our environment and dependencies.
Application File Structure
The file structure of our React Redux application is pretty simple.
├── package.json
├── src
│ ├── actions
│ ├── components
│ ├── index.html
│ ├── index.js
│ ├── reducers
│ └── store
├── tools
└── webpack.config.js
We have a package.json
since we'll use npm to manage dependencies.
The webpack.config.js
file will use webpack to bundle our app for the browser.
The tools
directory will house some of our supporting code, like the configuration of our server using Express.
The src
directory is where the magic happens, it contains the meat of our React Redux application.
React + Redux Structure
Let's take a moment to break down this structure.
Actions
Actions are JSON objects that contain information about changes that need to be made to state. They can be dispatched by various parts of your application, and they are received by the store.
Actions are produced by functions called action creators.
Store
The store holds the whole state our application. It can dispatch actions and it receives actions that are dispatched to it. However, the store doesn't want to handle the dispatched actions and actually enact changes to state. For that, we use reducers.
Reducers
The store passes dispatched actions to reducers, which receive the actions and make the appropriate changes to state.
Components
Components are the presentation, or view, layer of our application. Certain components, the parent or container components, will be connected to our store. Those components will be alerted whenever a change to state has been made by a reducer. Those components will then re-render, with new properties as dictated by the changes made to application state.
index.html
and index.js
The index.html
file provides the initial HTML for our app and includes a script tag that links to the bundled version of our app that webpack will create for us.
The index.js
file is the entry point for our React app. It will render the absolutely most top-level parent component and attach it to an element on our index.html
page.
Configuring React + Redux with Webpack
Let's briefly set up our application's dependencies and configure our server. We won't go too in-depth on this, we just want to get it up and running.
Configuring the Server
In tools/server.js
we'll set up our server.
import express from 'express'; import webpack from 'webpack'; import path from 'path'; import config from '../webpack.config.dev'; import open from 'open'; import favicon from 'serve-favicon'; /* eslint-disable no-console */ const port = 3000; const app = express(); const compiler = webpack(config); app.use(require('webpack-dev-middleware')(compiler, { noInfo: true, publicPath: config.output.publicPath })); app.use(require('webpack-hot-middleware')(compiler)); app.use(favicon(path.join(__dirname,'assets','public','favicon.ico'))); app.get('*', function(req, res) { res.sendFile(path.join( __dirname, '../src/index.html')); }); app.listen(port, function(err) { if (err) { console.log(err); } else { open(`http://localhost:${port}`); } });
Note that I am referencing a favicon stored in tools/assets/public/favicon.ico
. Feel free to eliminate that reference if you don't want to add your own charming cat favicon.
Using Webpack
Our webpack.config.js
has a few responsibilities:
- Bundle all of the application code in
src/
into a file,bundle.js
, stored in memory. (Storing in memory is fine for development mode). - Use certain plugins, like
HotModuleReplacementPlugin
for hot reloading.
import webpack from 'webpack'; import path from 'path'; export default { debug: true, devtool: 'cheap-module-eval-source-map', noInfo: false, entry: [ 'eventsource-polyfill', 'webpack-hot-middleware/client?reload=true', './src/index' ], target: 'web', output: { path: __dirname + '/dist', publicPath: '/', filename: 'bundle.js' }, devServer: { contentBase: './src' }, plugins: [ new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ], module: { loaders: [ {test: /\.js$/, include: path.join(__dirname, 'src'), loaders: ['babel']}, {test: /(\.css)$/, loaders: ['style', 'css']}, {test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file'}, {test: /\.(woff|woff2)$/, loader: 'url?prefix=font/&limit=5000'}, {test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/octet-stream'}, {test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=image/svg+xml'} ] } };
Application Dependencies
Our package.json
specifies our application dependencies and scripts. You can check out my complete package.json
for this project here.
Final Touches: .babelrc
and .eslintrc
We need to set up a .babelrc
file to tell our app to use React and ES2016.
// .bablerc
{
"presets": ["react", "es2015"],
"env": {
"development": {
"presets": ["react-hmre"]
},
"production": {
"presets": ["react", "es2015"]
}
}
}
Lastly, we need to set up our linting rules, which we'll do in the .eslintrc
, in the root of our project.
{
"extends": [
"eslint:recommended",
"plugin:import/errors",
"plugin:import/warnings"
],
"plugins": [
"react"
],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"env": {
"es6": true,
"browser": true,
"node": true,
"jquery": true,
"mocha": true
},
"rules": {
"quotes": 0,
"no-console": 1,
"no-debugger": 1,
"no-var": 1,
"semi": [1, "always"],
"no-trailing-spaces": 0,
"eol-last": 0,
"no-unused-vars": 0,
"no-underscore-dangle": 0,
"no-alert": 0,
"no-lone-blocks": 0,
"jsx-quotes": 1,
"react/display-name": [ 1, {"ignoreTranspilerName": false }],
"react/forbid-prop-types": [1, {"forbid": ["any"]}],
"react/jsx-boolean-value": 1,
"react/jsx-closing-bracket-location": 0,
"react/jsx-curly-spacing": 1,
"react/jsx-indent-props": 0,
"react/jsx-key": 1,
"react/jsx-max-props-per-line": 0,
"react/jsx-no-bind": 1,
"react/jsx-no-duplicate-props": 1,
"react/jsx-no-literals": 0,
"react/jsx-no-undef": 1,
"react/jsx-pascal-case": 1,
"react/jsx-sort-prop-types": 0,
"react/jsx-sort-props": 0,
"react/jsx-uses-react": 1,
"react/jsx-uses-vars": 1,
"react/no-danger": 1,
"react/no-did-mount-set-state": 1,
"react/no-did-update-set-state": 1,
"react/no-direct-mutation-state": 1,
"react/no-multi-comp": 1,
"react/no-set-state": 0,
"react/no-unknown-property": 1,
"react/prefer-es6-class": 1,
"react/prop-types": 1,
"react/react-in-jsx-scope": 1,
"react/require-extension": 1,
"react/self-closing-comp": 1,
"react/sort-comp": 1,
"react/wrap-multilines": 1
}
}
Running the App
You should be able to run npm install
and then start the server via npm start
.
Now that our basic application structure is in place, we're ready to start building in React!