Re-energize Your
Workflow
with React and Redux
Jonathan Kemp
@jonkemp
Jonathan Kemp
Senior Front End Engineer
@ Scripps Networks
Knoxville, TN
The Kemp Family
Follow Along:
Slides - https://slides.com/jonkemp/react-redux/live
Repo - https://github.com/jonkemp/recalculator
Why React?
Why REact?
- Build user interfaces
- Reusable UI components
- Data changes over time
- Large applications
What Problems Does
React solve?
Problems:
- Modifying the DOM is expensive
- Managing state changes in the DOM is complex
Solutions:
- Performs updates as efficiently as possible
- Manages UI updates for you
Fast
- React makes use of a virtual DOM.
- When data changes, React decides if an update is needed by building a new virtual DOM and comparing (diffing) them.
- React only updates the parts that changed.
the React ecosystem:
What you should LEarn
In This Order:
- React itself
- npm
- JavaScript "bundlers"
- ES6
- Routing
- Flux
You don't need to learn all of these
to be productive with React.
Learning REact
Misconception:
You need to spend a lot of time setting up tooling to start to learn React.
Reality:
- No tooling is required.
- Learn the tooling once you are comfortable with the basics.
Components
Problem
Expressing display logic in template languages can be cumbersome
Components
- No templates
- Markup and logic are tied together
JSX
create JavaScript objects
using HTML syntax
* Not Required
Simple
- JSX provides the readability of HTML
- "Designers" can contribute React code with JSX
// To generate a link in React using pure JavaScript:
React.createElement('a', {href: 'https://facebook.github.io/react/'}, 'Hello!');
// With JSX this becomes:
<a href="https://facebook.github.io/react/">Hello!</a>
Updates
- Dynamic data is represented as "props"
- Passed as attributes in JSX syntax
- Handles "state"
Best PRactices
- Most components should be stateless
- Parent components manage state
- Children just render the data as props
Demo Time
Building Complex Applications
With React
NPM
- The most popular way to share JavaScript
- Most reusable components, libraries and tools in the React ecosystem are installed with npm
- CommonJS modules (i.e. everything in npm) cannot be used natively in the browser
JavaScript Bundlers
JavaScript Bundlers
JavaScript “bundlers” “bundle” these modules into .js files that you can include in your web page with a <script> tag
Examples
- Webpack
- Browserify
Why Webpack?
Webpack
- Split your app into multiple files
- Often replaces grunt or gulp
- Supports AMD, CommonJS, among other module systems (Angular, ES6)
// webpack.config.js
module.exports = {
entry: [
'webpack/hot/dev-server',
'webpack-dev-server/client?http://localhost:8080',
path.resolve(__dirname, 'app/main.js')
],
output: {
path: path.resolve(__dirname, 'build'),
filename: 'bundle.js'
},
devtool: 'source-map',
module: {
loaders: [
{
test: /\.js$/,
loader: 'babel-loader',
query: {
presets: ['es2015', 'react']
}
},
{ test: /\.css$/, loader: 'style-loader!css-loader' }
]
}
};
Webpack Config
{
"name": "recalculator",
"version": "1.0.0",
"description": "This is an example app built with React and Redux.",
"main": "app/main.js",
"scripts": {
"build": "webpack",
"dev": "webpack-dev-server --devtool eval --progress --colors --hot --content-base build",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": "jonkemp/recalculator",
"author": "Jonathan Kemp <kempdogg@gmail.com> (http://jonkemp.com/)",
"license": "BSD-2-Clause",
"dependencies": {
"bootstrap": "^3.3.6",
"pubsub-js": "^1.5.3",
"react": "^15.1.0",
"react-dom": "^15.1.0",
"react-redux": "^4.4.5",
"redux": "^3.5.2"
},
"devDependencies": {
"babel-core": "^6.9.1",
"babel-loader": "^6.2.0",
"babel-preset-es2015": "^6.9.0",
"babel-preset-react": "^6.5.0",
"css-loader": "^0.23.1",
"file-loader": "^0.9.0",
"style-loader": "^0.13.1",
"url-loader": "^0.5.7",
"webpack": "^1.13.1",
"webpack-dev-server": "^1.14.1"
}
}
ES6
major upgrade to the language
* Not Required
Learning ES6
- If you just want to get things done with React, skip this
- Not necessarily the preferred way (docs in ES5)
- Not supported in browsers yet (your bundler can translate it for you)
Routing
Learning routing
- Useful for Single-page Applications
- Most popular router in the React ecosystem is 'react-router'
- Don’t use a router if you aren’t building a SPA
Flux
Do I need Flux?
- React is good at managing state.
- Flux should only be added once many components have already been built.
- If you aren’t sure if you need it, you don’t need it.
What problems does Flux solve?
- Managing state in components is an anti pattern
- Makes maintenance and adding new features easier
- Makes data changes easy to reason about
the most popular
AND WELL-DOCUMENTED FLUX LIBRARY
IS REDUX.
REDUX
* Not Required
Problems to Solve
As application complexity grows, managing state becomes harder.
At some point, you no longer understand what happens in your app as you have lost control over the when, why, and how of state changes.
WHAT IS REDUX?
- A state container that will help your applications
behave consistently - Avoids the complexity found in Flux
- A functional programming approach (to Flux)
Redux Overview
- State of your app is stored in an object tree inside a single store.
- Only way to change the state tree is to emit an action, an object describing what happened.
- To specify how the actions transform the state tree, you write pure reducers.
- That’s it!
import { createStore } from 'redux';
/**
* This is a reducer, a pure function with (state, action) => state
* signature. It describes how an action transforms the state into
* the next state.
*/
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
// Create a Redux store holding the state of your app.
// Its API is { subscribe, dispatch, getState }.
let store = createStore(counter)
// You can use subscribe() to update the UI in response to state
// changes.
store.subscribe(() =>
console.log(store.getState())
)
// The only way to mutate the internal state is to dispatch an action.
// The actions can be serialized, logged or stored and later replayed.
store.dispatch({ type: 'INCREMENT' })
// 1
store.dispatch({ type: 'INCREMENT' })
// 2
store.dispatch({ type: 'DECREMENT' })
// 1
The Gist
Benefits
- Scales well to large and complex apps
- Enables very powerful developer tools
- Applications that behave consistently
- Run in different environments (client, server, and native)
- Easy to test
Differences from Flux
- Redux doesn’t have a Dispatcher or support many stores.
- Instead, there is just a single store with a single root reducing function.
- Instead of adding stores, you split the root reducer into smaller reducers.
Get Started with Redux
- Can be used with any view library, independent of React
- Babel or module bundler not required
- Install via npm
Three Principles of Redux
Single source of truth
The state of your whole application is stored in an object tree within a single store.
console.log(store.getState())
/* Prints
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
*/
Single source of truth
A single state tree also makes it easier to debug or introspect an application.
State is read-only
The only way to mutate the state is to emit an action, an object describing what happened.
store.dispatch({
type: 'COMPLETE_TODO',
index: 1
})
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: 'SHOW_COMPLETED'
})
State is read-only
This ensures that neither the views nor the network callbacks will ever write directly to the state.
Changes are made with pure functions
To specify how the state tree is transformed by actions, you write pure reducers.
function visibilityFilter(state = 'SHOW_ALL', action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
text: action.text,
completed: false
}
]
case 'COMPLETE_TODO':
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: true
})
}
return todo
})
default:
return state
}
}
import { combineReducers, createStore } from 'redux'
let reducer = combineReducers({ visibilityFilter, todos })
let store = createStore(reducer)
Reducers
- Pure functions
- Take the previous state and an action,
and return the next state - Return new state objects, instead of
mutating the previous state
Recalculator
Features:
- History tape
- Every calculation gets saved to the history tape
- Refer to calculations later or send them right back to the calculator
- Allows you to see everything you have typed
Actions
Actions:
- Actions are payloads of information that send data from your application to your store.
- Actions describe the fact that something happened.
- They are the only source of information for the store.
- Send them to the store using store.dispatch().
Action Types
- Actions must have a type property that indicates the type of action being performed.
- Types should typically be defined as string constants.
Action Creators
- Functions that return an action
- Pass the result to the dispatch() function
- Can be asynchronous and have side-effects
/*
* action types
*/
export const ADD_ENTRY = 'ADD_ENTRY';
export const REMOVE_ENTRY = 'REMOVE_ENTRY';
export const UPDATE_RESULT = 'UPDATE_RESULT';
/*
* action creators
*/
export function addEntry(text) {
return { type: ADD_ENTRY, text };
}
export function removeEntry(index) {
return { type: REMOVE_ENTRY, index };
}
export function updateResult(result) {
return { type: UPDATE_RESULT, result };
}
Reducers
Reducers
Reducers specify how the application’s state changes in response to actions.
Application State
- Stored as a single object
- Store data, as well as UI state
// Application State
{
items:
[
{
id: 1,
expression: "5 * 3",
result: 15
},
{
id: 0,
expression: "5 * 2",
result: 10
}
],
lastResult: 15
}
Reducers should:
- Take the previous state and an action, and return the next state.
- Given the same arguments, it should calculate the next state and return it.
Reducers should Not:
- Mutate arguments
- Perform side effects
import { ADD_ENTRY, REMOVE_ENTRY, UPDATE_RESULT }
from '../actions/index';
function calculateApp(state, action) {
switch (action.type) {
case ADD_ENTRY:
return Object.assign({}, state, {
items: [
{
id: state.items.reduce((maxId, todo) =>
Math.max(todo.id, maxId), -1) + 1,
expression: action.text,
result: eval(action.text)
},
...state.items
]
})
}
}
export default calculateApp;
case REMOVE_ENTRY:
return Object.assign({}, state, {
items: [
...state.items.slice(0, action.index),
...state.items.slice(action.index + 1)
]
})
case UPDATE_RESULT:
return Object.assign({}, state, {
lastResult: action.result
})
Object.assign()
- Copies the values of all enumerable own properties from one or more source objects to a target object
- Returns the target object
Object Spread Syntax ...
- Copy enumerable properties from one object to another in a more succinct way
- Easy to return a new object
Splitting Reducers
- Split the reducers into separate files
- Independent, managing different data domains
- Redux provides a utility called combineReducers()
import { combineReducers } from 'redux'
const calculateApp = combineReducers({
items,
lastResult
})
export default calculateApp
Redux Store
Redux Store
Holds the state and takes care of calling your reducer when you dispatch an action
The Store
- Allows access to state via getState()
- Allows state to be updated via dispatch(action)
- Registers listeners via subscribe(listener)
import { createStore, applyMiddleware, compose } from 'redux';
import calculateApp from './reducers/index';
const localStore = store => next => action => {
const result = next(action);
localStorage.setItem('recalculator', JSON.stringify(store.getState()));
return result;
};
const data = JSON.parse(localStorage.getItem('recalculator')) ||
{ items: [], lastResult: 0 },
store = createStore(
calculateApp,
data,
compose(
applyMiddleware(localStore)
)
);
Data Flow
Data Flow
- Dispatch an action
- Store calls the reducer
- Reducer returns the new state
React Redux
React Bindings for Redux
Presentational and Container Components
Container Components
- Aware of Redux
- Subscribe to Redux state
- Dispatch Redux actions
Presentational Components
- Not aware of Redux
- Read data from props
- Invoke callbacks from props
PRESENTATIONAL COMPONENTS
import React, { PropTypes } from 'react';
import Entry from './Entry';
const EntryList = ({ items, remove }) => (
<div className="row">
<ul className="list-group">
{items.map((item, index) => {
return <Entry key={item.id} {...item} remove={() =>
remove(index)} />;
})}
</ul>
</div>
)
export default EntryList;
import React, { PropTypes } from 'react';
import PubSub from 'pubsub-js';
const Entry = React.createClass({
render() {
return (
<li className="list-group-item">
<div>{this.props.result}</div>
<div>{this.props.expression}</div>
<button className="btn btn-warning btn-xs" type="button"
onClick={() => {
PubSub.publish('entry', this.props.result);
}}>Use Result</button>
<button className="btn btn-warning btn-xs" type="button"
onClick={() => {
PubSub.publish('entry', this.props.expression);
}}>Use Expression</button>
<button className="btn btn-danger btn-xs" type="button"
onClick={this.props.remove}>Remove</button>
</li>
)
}
});
export default Entry;
import React, { PropTypes } from 'react';
import PubSub from 'pubsub-js';
const EntryForm = React.createClass({
componentWillMount() {
this.pubsub_token = PubSub.subscribe('entry', (topic, value) => {
this.input.value = value;
this.handleChange();
});
},
componentWillUnmount() {
PubSub.unsubscribe(this.pubsub_token);
},
handleChange() {
let value;
try {
value = this.input.value !== '' ? eval(this.input.value) : 0;
this.props.change(value);
} catch (e) {
// statements to handle any exceptions
}
},
render() {
return (
<div className="row">
<h3>Recalculator</h3>
<h1>{this.props.result} <button className="btn btn-success" type="button"
onClick={(e) => {
this.input.value = this.props.result;
}}><span className="glyphicon glyphicon-arrow-down" aria-hidden="true"></span>
</button></h1>
<form className="form-inline" onSubmit={(e) => {
e.preventDefault();
this.props.submit(this.input.value);
this.input.value = '';
}}>
<div className="form-group">
<input className="form-control" type="text" ref={node => {
this.input = node
}} onChange={this.handleChange} />
</div>
<button className="btn btn-primary" type="submit">Submit</button>
</form>
</div>
);
}
});
export default EntryForm;
CONTAINER COMPONENTS
React Redux API
React Redux API
- connect()
- mapStateToProps()
- mapDispatchToProps()
generating container components
With the React REdux API
mapStateToProps
Tells how to transform the current Redux store state into the props you want to pass to a presentational component.
import { connect } from 'react-redux';
import { addEntry, updateResult } from '../actions/index';
import EntryForm from '../components/EntryForm';
const mapStateToProps = (state) => {
return {
result: state.lastResult
}
};
const mapDispatchToProps = (dispatch) => {
return {
submit: (value) => {
dispatch(addEntry(value));
},
change: (value) => {
dispatch(updateResult(value));
}
}
}
const AddEntry = connect(
mapStateToProps,
mapDispatchToProps
)(EntryForm);
export default AddEntry;
mapDispatchToProps
Receives the dispatch() method and returns callback props that you want to inject into the presentational component.
import { connect } from 'react-redux';
import { removeEntry } from '../actions';
import EntryList from '../components/EntryList';
const mapStateToProps = (state) => {
return {
items: state.items
}
};
const mapDispatchToProps = (dispatch) => {
return {
remove: (index) => {
dispatch(removeEntry(index));
}
}
}
const PushEntryList = connect(
mapStateToProps,
mapDispatchToProps
)(EntryList);
export default PushEntryList;
Summary
React
Performs updates as efficiently as possible.
Manages UI updates for you.
React
Tooling is not required to get started.
Learn the tooling once you are comfortable with the basics.
Redux
Goal is to help your applications behave consistently and make state changes easier to reason about.
For more information:
React - https://facebook.github.io/react/
Redux - http://redux.js.org/
react-webpack-template - https://github.com/petehunt/react-webpack-template
webpack-howto - https://github.com/petehunt/webpack-howto
Resources:
Slides - https://slides.com/jonkemp/react-redux
Repo - https://github.com/jonkemp/recalculator
Thank YOU!
Re-energize Your Workflow with React and Redux
By Jonathan Kemp
Re-energize Your Workflow with React and Redux
- 688