Jonathan Kemp
@jonkemp
@ Scripps Networks
Slides - https://slides.com/jonkemp/react-redux/live
Repo - https://github.com/jonkemp/recalculator
You need to spend a lot of time setting up tooling to start to learn React.
Expressing display logic in template languages can be cumbersome
* Not Required
// 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>
JavaScript “bundlers” “bundle” these modules into .js files that you can include in your web page with a <script> tag
// 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' }
]
}
};
{
"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"
}
}
* Not Required
* Not Required
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.
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 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
}
]
}
*/
A single state tree also makes it easier to debug or introspect an application.
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'
})
This ensures that neither the views nor the network callbacks will ever write directly to the state.
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)
/*
* 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 specify how the application’s state changes in response to actions.
// Application State
{
items:
[
{
id: 1,
expression: "5 * 3",
result: 15
},
{
id: 0,
expression: "5 * 2",
result: 10
}
],
lastResult: 15
}
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()
Object Spread Syntax ...
import { combineReducers } from 'redux'
const calculateApp = combineReducers({
items,
lastResult
})
export default calculateApp
Holds the state and takes care of calling your reducer when you dispatch an action
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)
)
);
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;
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;
Performs updates as efficiently as possible.
Manages UI updates for you.
Tooling is not required to get started.
Learn the tooling once you are comfortable with the basics.
Goal is to help your applications behave consistently and make state changes easier to reason about.
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
Slides - https://slides.com/jonkemp/react-redux
Repo - https://github.com/jonkemp/recalculator