The State of State
Joe Fleming
Dev @ Elastic
@w33ble
Local vs Global State
Local state is fine
Redux [adds] indirection to decouple “what happened” from “how things change”
- Dan Abramov, You Might Not Need Redux
When to Add App State
When [you] have multiple components that share common state
Local State Only
Shared State in Leaf Nodes
Global State Manager
Local State Counter
import React, { Component } from 'react';
class Counter extends Component {
state = { value: 0 };
increment = () => {
this.setState(prevState => ({
value: prevState.value + 1
}));
};
decrement = () => {
this.setState(prevState => ({
value: prevState.value - 1
}));
};
render() {
return (
<div>
{this.state.value}
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
</div>
)
}
}
Reducer plus setState
import React, { Component } from 'react';
const counter = (state = { value: 0 }, action) => {
switch (action.type) {
case 'INCREMENT':
return { value: state.value + 1 };
case 'DECREMENT':
return { value: state.value - 1 };
default:
return state;
}
}
class Counter extends Component {
state = counter(undefined, {});
dispatch(action) {
this.setState(prevState => counter(prevState, action));
}
increment = () => {
this.dispatch({ type: 'INCREMENT' });
};
decrement = () => {
this.dispatch({ type: 'DECREMENT' });
};
render() {
return (
<div>
{this.state.value}
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
</div>
)
}
}
Stateless Component
import React, { Component } from 'react';
const counter = (state = { value: 0 }, action) => {
switch (action.type) {
case 'INCREMENT':
return { value: state.value + 1 };
case 'DECREMENT':
return { value: state.value - 1 };
default:
return state;
}
}
const StatelessCounter = ({ value, increment, decrement }) => {
return (
<div>
{value}
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
class Counter extends Component {
state = counter(undefined, {});
dispatch(action) {
this.setState(prevState => counter(prevState, action));
}
increment = () => {
this.dispatch({ type: 'INCREMENT' });
};
decrement = () => {
this.dispatch({ type: 'DECREMENT' });
};
render() {
return (
<StatelessCounter value={this.state.value} increment={this.increment} decrement={decrement} />
);
}
}
Redux
Redux is a predictable state container for JavaScript apps.
Redux Principles
Describe application state as plain objects
and arrays, single source of truth
You can use an initial state object to describe the entire application
Redux Principles
Describe changes in the system as plain
objects, state is read-only
Actions are dispatched from component, or other actions
Redux Principles
Describe the logic for handling changes as
pure functions
State is updated using reducers, which accept an accumulation and a value and returns a new accumulation
Redux Principles
Initial State
{
transient: { // does not survive a refresh
selectedElement: 'element-d88c-4bbd-9453-db22e949b92e',
selectedPage: 'page-f3ce-4bb7-86c8-0417606d6592',
resolvedArgs: {
'element-d88c-4bbd-9453-db22e949b92e': {
expressionContexts: {
state: 'ready',
value: {
type: 'datatable',
columns: ['id', 'username', 'time', 'cost', 'price'],
},
error: null,
},
},
},
},
persistent: { // survives refresh, be serialized and be saved
workpad: {
name: 'Untitled Workpad',
id: 'workpad-2235-4af1-b781-24e42a453e5b',
pages: [{
id: 'page-f3ce-4bb7-86c8-0417606d6592',
elements: [{
id: 'element-d88c-4bbd-9453-db22e949b92e',
expression: 'demodata().sort("time").pointseries(x="time", y=.math("sum(price)")).line()',
ast: '...',
}],
}],
},
},
}
Actions Describe Changes
import { ADD_TODO } from '../actionTypes'
// action creator
export addTodo = (todo) => ({
type: ADD_TODO,
payload: todo,
})
Reducers Make New State
import { ADD_TODO } from '../actionTypes'
export default function todoReducer((state, action) => {
switch(action.type) {
case ADD_TODO:
// handle the request action
return Object.assign({}, state, {
todos: state.todos.concat(action.payload),
});
default:
return state;
}
});
Redux Complications
Async === Advanced
- Redux treats async as an advanced topic
- There's a slew of middleware and even action creators to try to help
- Most rely on redux-thunk
- Everyone writes their own helpers, just like I did
Dispatching From Actions
- Particularly important for async operations
- Even in sync operations, it's really handy
- Smaller, isolated actions
Async & Dispatching
import fetch from 'axios';
import { ADD_TODO_SUCCESS, ADD_TODO_ERROR, ADD_TODO_REQUEST } from '../actionTypes';
// action creator
export addTodo = (todo) => (dispatch, getState) => {
fetch.get('...')
.then((data) => {
dispatch({
type: ADD_TODO_SUCCESS,
payload: data,
});
})
.catch((err) => {
dispatch({
type: ADD_TODO_ERROR,
error: err,
payload: null,
});
});
return {
type: ADD_TODO_REQUEST,
payload: null,
};
}
};
Deep State Trees
- Redux docs tell you to normalize your state
- Reducers should not know the full state tree
- You can use combineReducers to build the tree
- Mixes well with reduce-reducers to split reducer logic apart
Deep State Trees
import thunk from 'redux-thunk';
import combineReducers from 'redux';
import initialState from './initial_state';
import * as reducers from './reducers';
const rootReducer = combineReducers({
app: reducers.appReducer,
transient: combineReducers({
properties: reducers.transientReducer,
resolvedArgs: reducers.resolvedArgsReducer,
}),
persistent: combineReducers({
workpad: combineReducers({
properties: reducers.workpadReducer,
pages: combineReducers({
properties: reducers.pagesReducer,
elements: reducers.elementsReducer,
})
})
}),
});
const store = createStore(rootReducer, initialState, applyMiddleware(thunk));
Vuex
Vuex is a state management pattern and library for Vue.js applications
Vuex Principles
Describe application state using a global
singleton that enforces rules for changes
You can use an initial state object to describe the entire application
Vuex Principles
Changes in the system happen by dispatching
actions and committing mutations
State is mutated in the global singleton, using predictable rules
Vuex Principles
A library implementation that takes advantage of Vue's granular reactivity system
Mutations are done on observable objects that trigger updates in components
Vuex Principles
Vuex Store
import Vuex from 'vuex';
const store = new Vuex.Store({
state: {
count: 0,
},
mutations: {
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
},
},
});
Vuex Async
import Vuex from 'vuex';
import fetch from 'axios';
const store = new Vuex.Store({
state: {
todos: [],
},
mutations: {
pushTodo(state, todo) {
state.todos.push(todo);
},
},
actions: {
addTodo({ commit }) {
return fetch.get('...')
.then(data => commit('pushTodo', data));
},
},
});
Vuex Async + Dispatch
import Vuex from 'vuex';
import fetch from 'axios';
const store = new Vuex.Store({
state: {
todos: [],
},
mutations: {
pushTodo(state, todo){
state.todos.push(todo);
},
},
actions: {
addTodo({ commit, dispatch }) {
return fetch.get('...')
.then(data => {
dispatch('anotherAction');
commit('pushTodo', data)
});
},
},
});
Vuex in Vue Component
import Vue from 'vue';
var todoApp = new Vue({
el: '#todoApp',
data: {
working: false,
}
computed: {
todos() {
return this.$store.todos;
},
},
methods: {
addTodo: function (todo) {
this.working = true;
this.$store.dispatch('addTodo', todo)
.then(() => { this.working = false; });
},
},
});
Vue + Vuex
If I was going to sum up my experiences with Vue in a sentence, I'd probably say something like "it's just so reasonable"
- Sarah Drasner, Intro to Vue.js
Learning Vue, Part 2
vue-router and vuex
Next Month!
June 28th
State of State
By Joe Fleming
State of State
A look at Redux and Vuex
- 2,080