A predictable state container for JavaScript apps
What are the advertised benefits to a functional approach?
PluralSight - Functional Programming with C# (1.5 hours)
function add(a, b) {
return a + b;
}
add(5, 5);
//returns 10
var state = { total: 0 };
function impureAdd(a, b) {
state.total = a + b;
return state.total;
}
impureAdd(5, 5, state);
//returns 10
function calc(a, b, command) {
if(command === "ADD") {
return a + b;
} else if(command === "SUBTRACT") {
return a - b;
}
}
calc(5, 5, "ADD");
//returns 10
//This reducer is impure
function reducer(state, action) {
switch(action.type) {
case 'ADD_COUNTER':
state.total += action.value;
return state;
}
}
var stateBefore = { total: 5 };
var action = {type: 'ADD_COUNTER', value: 5 }
var stateAfter = reducer(stateBefore, action);
console.log(stateAfter.total);
//returns 10
//This reducer is pure
function reducer(state, action) {
switch(action.type) {
case 'ADD_COUNTER':
var newState = {};
newState.total = state.total + action.value;
return newState;
}
}
var stateBefore = { total: 5 };
var action = {type: 'ADD_COUNTER', value: 5 }
var stateAfter = reducer(stateBefore, action);
console.log(stateAfter.total);
//returns 10
//ES6 Object.assign
function reducer(state, action) {
switch(action.type) {
case 'ADD_COUNTER':
var newState = Object.assign({}, state);
newState.total = state.total + action.value;
return newState;
}
}
var stateBefore = { total: 5 };
var action = {type: 'ADD_COUNTER', value: 5 }
var stateAfter = reducer(stateBefore, action);
console.log(stateAfter.total);
//returns 10
//ES next object spread
function reducer(state, action) {
switch(action.type) {
case 'ADD_COUNTER':
return {...state, total: state.total + action.value };
}
}
var stateBefore = { total: 5 };
var action = {type: 'ADD_COUNTER', value: 5 }
var stateAfter = reducer(stateBefore, action);
console.log(stateAfter.total);
//returns 10
// Bring in Redux!
var createStore = require('redux').createStore;
// Define a reducer
function reducer(state, action) {
if(!action.type) { return state; }
switch(action.type) {
case 'ADD_COUNTER':
return {...state, total: state.total + action.value };
default:
return state;
}
}
var store = createStore(reducer, { total: 5 });
store.dispatch({type: 'ADD_COUNTER', value: 5 }); //10
store.dispatch({type: 'ADD_COUNTER', value: 5 }); //15
console.log(store.getState()); //{ total: 15 }
// Bring in Redux!
import { createStore } from 'redux';
// Define a reducer
function reducer(state, action) {
if(!action.type) { return state; }
switch(action.type) {
case 'ADD_COUNTER':
return {...state, total: state.total + action.value };
default:
return state;
}
}
const store = createStore(reducer, { total: 5 });
store.subscribe(function() {
console.log('Value changed!', store.getState());
});
store.dispatch({type: 'ADD_COUNTER', value: 5 });
Using nothing but Redux and the DOM
// Inside your app
// <button onclick="dispatchAction()">Do Work</button>
function dispatchAction() {
//Show loading indicator
store.dispatch({ type: "REQUEST_STARTED" });
//Make AJAX call
$.ajax({ url: "/foo/bar" })
.done((data) => {
//Remove loading indicator, update with new data
store.dispatch({ type: "REQUEST_COMPLETED", value: data });
});
}
function dispatchAction() {
//Show loading indicator
store.dispatch({ type: "REQUEST_STARTED" });
//Perform synchronous validation
store.dispatch({ type: "VALIDATE_FORM" });
if(!store.getState().isValid) { return; }
//Make AJAX call
$.ajax({ url: "/foo/bar" })
.done((data) => {
//Remove loading indicator, update with new data
store.dispatch({ type: "REQUEST_COMPLETED", value: data });
});
}
//ES next goodness
async function emailChanged(email) {
store.dispatch({ type: "UPDATE_EMAIL", value: email });
const emailValid = await $.ajax({ url: "/is/email/valid" });
store.dispatch({ type: "EMAIL_VALID_RECD", value: emailValid });
}
"App"
Controller View
Store
State
Child
Component
Child
Component
Child
Component
Reducer(s)
UI components only reflect the state passed to them.
Logic lives in the reducers.
dispatch
subscribe
//Reducer 1
export default function todos(state = [], action) { ... }
//Reducer 2
export default function counter(state = 0, action) { ... }
//Combine Reducers
const combinedReducer = combineReducers({ todos, counter });
//Create the store
const store = createStore(combinedReducer);
store.getState(); // returns { todos: [], counter: 0 }
//Each reducer is handed its own branch of the state tree
// actionTypes.js
export default {
ADD_ITEM: "ADD_ITEM",
REMOVE_ITEM: "REMOVE_ITEM",
EMPTY_CART: "EMPTY_CART",
CART_LOADED: "CART_LOADED",
CREATING_ORDER: "CREATING_ORDER",
ORDER_CREATED: "ORDER_CREATED"
}
This is for apps that have large teams divided by products that ship sub-apps within a single enclosing "app shell".
// actions.js
export function addCounter(amount) {
return { type: actionTypes.ADD_COUNTER, value: amount };
}
export function resetCounter() {
return { type: actionTypes.RESET_COUNTER };
}
// app file
import * as actions from './actions';
//store.dispatch({ type: 'ADD_COUNTER', value: 5 });
store.dispatch(actions.addCounter(5));
Action Creators help define the shape of your actions
// What is a thunk?
// In this context it's...
// 1. a function that returns a function
function thunk() {
/* outer function */
return function() { /* inner function */ };
}
// 2. that dispatches actions
function saveProfile() {
return function (dispatch, getState) {
dispatch(actions.savingProfile());
$.ajax({...})
.done(() => dispatch(actions.profileSaved()));
};
}
// Inside your app
import * as actions from './actions';
// <button onclick="store.dispatch(actions.doWorkThunk())">...
// inside ./actions
function doWorkThunk() {
return (dispatch, getState) => {
//Show loading indicator
store.dispatch({ type: "REQUEST_STARTED" });
//Make AJAX call
$.ajax({ url: "/foo/bar" })
.done((data) => {
//Remove loading indicator, update with new data
store.dispatch({ type: "REQUEST_COMPLETED", value: data });
});
}
}
Connect, Mapping Dispatch, Mapping State, Binding Action Creators