Advanced State Management
Advanced State Management
Steve Kinney
A Frontend Masters Workshop
Hi, I’m Steve.
(@stevekinney)
We’re going to talk about
state.
To build our understanding of how to
manage state, we’re going to take a
whirlwind tour of a number of approaches.
We’re going to start from the
very basics and work our way up.
-thunk
But, this workshop is about
more than just the libraries.
Libraries come and go.
Patterns and approaches
stick around.
Managing UI state is not a solved
problem. New ideas and
implementations will come along.
My goal is to help you think about and
apply these conceptual patterns, regardless
of what library is the current flavor.
My goal is that if something new comes
along, you’re able to apply concepts
you learned during this workshop.
Part One
Understanding State
The main job of React is to take
your application state and turn it
into DOM nodes.
“
The key here is DRY: Don’t Repeat Yourself. Figure out the
absolute minimal representation of the state your
application needs and compute everything else you need
on-demand.
“
1. Is it passed in from a parent via props? If so, it probably isn’t state.
2. Does it remain unchanged over time? If so, it probably isn’t state.
3. Can you compute it based on any other state or props in your
component? If so, it isn’t state.
“
Remember: React is all about one-way data flow down the
component hierarchy. It may not be immediately clear which
component should own what state. This is often the most
challenging part for newcomers to understand.
props vs. state
State is created in the component
and stays in the component. It can
be passed to a children as its props.
class Counter extends Component {
state = { count: 0 }
render() {
const { count } = this.state;
return (
<div>
<h2>This is my state: { count }.!</h2>
<DoubleCount count={count} !/>
!</div>
)
}
}
• Ephemeral state: Stuff like the value of an input field that will
be wiped away when you hit “enter.”
Ask yourself: Does a input field need
the same kind of state management as
your model data?
Ask yourself: What about
form validation?
Ask yourself: Does it make sense to
store all of your data in one place or
compartmentalize it?
Spoiler alert: There is no
silver bullet.
Part Two
An Uncomfortably Close Look
at React Component State
Let’s start with the world’s
simplest React component.
Exercise
• Okay, this is going to be a quick one: We’re going to implement a little counter.
• https://github.com/stevekinney/basic-counter
• Increment
• Decrement
• Reset
render() { … }
}
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
console.log(this.state.count);
Any guesses?
0
this.setState() is
asynchronous.
But, why?
React is trying to avoid
unnecessary re-renders.
export default class Counter extends Component {
constructor() {
super();
this.state = { count: 0 };
this.increment = this.increment.bind(this);
}
increment() {…}
render() {
return (
<section>
<h1>Count: {this.state.count}!</h1>
<button onClick={this.increment}>Increment!</button>
!</section>
)
}
}
export default class Counter extends Component {
constructor() { … }
increment() {
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
}
render() { … }
}
What will the count be after the
user’s clicks the “Increment”
button?
1
Effectively, you’re queuing up
state changes.
React will batch them up, figure out
the result and then efficiently make
that change.
Alright, here is the second
question…
Object.assign(
{},
yourFirstCallToSetState,
yourSecondCallToSetState,
yourThirdCallToSetState,
);
There is actually a bit more
to this.setState().
Fun fact: Did you know that you can
also pass a function in as an
argument?
import React, { Component } from 'react';
increment() {
this.setState((state) !=> { return { count: state.count + 1 } });
this.setState((state) !=> { return { count: state.count + 1 } });
this.setState((state) !=> { return { count: state.count + 1 } });
}
render() { … }
}
Any guesses here?
3
When you pass functions to
this.setState(), it plays
through each of them.
But, the more useful feature is that
it gives you some programmatic
control.
import React, { Component } from 'react';
increment() {
this.setState(state !=> {
if (state.count !>= 5) return;
return { count: state.count + 1 };
})
}
render() { … }
}
this.setState also takes
a callback.
import React, { Component } from 'react';
increment() {
this.setState(
{ count: this.state.count + 1 },
() !=> { console.log(this.state); }
)
}
render() { … }
}
Patterns and anti-patterns
State should be considered
private data.
When we’re working with props, we
have PropTypes. That’s not the
case with state.
Don’t use this.state for
derivations of props.
class User extends Component {
constructor(props) {
super(props);
this.state = {
fullName: props.firstName + ' ' + props.lastName
};
}
}
Don’t do this. Instead, derive
computed properties directly from
the props themselves.
class User extends Component {
render() {
const { firstName, lastName } = this.props;
const fullName = firstName + ' ' + lastName;
return (
<h1>{fullName}!</h1>
);
}
}
!// Alternatively…
class User extends Component {
get fullName() {
const { firstName, lastName } = this.props;
return firstName + ' ' + lastName;
}
render() {
return (
<h1>{this.fullName}!</h1>
);
}
}
You don’t need to shove everything
into your render method.
You can break things out into
helper methods.
class UserList extends Component {
render() {
const { users } = this.props;
return (
<section>
<VeryImportantUserControls !/>
{ users.map(user !=> (
<UserProfile
key={user.id}
photograph={user.mugshot}
onLayoff={handleLayoff}
!/>
)) }
<SomeSpecialFooter !/>
!</section>
);
}
}
class UserList extends Component {
renderUserProfile(user) {
return (
<UserProfile
key={user.id}
photograph={user.mugshot}
onLayoff={handleLayoff}
!/>
)
}
render() {
const { users } = this.props;
return (
<section>
<VeryImportantUserControls !/>
{ users.map(this.renderUserProfile) }
<SomeSpecialFooter !/>
!</section>
);
}
}
Don’t use state for things
you’re not going to render.
class TweetStream extends Component {
constructor() {
super();
this.state = {
tweets: [],
tweetChecker: setInterval(() !=> {
Api.getAll('/api/tweets').then(newTweets !=> {
const { tweets } = this.state;
this.setState({ tweets: [ !!...tweets, newTweets ] });
});
}, 10000)
}
}
componentWillUnmount() {
clearInterval(this.state.tweetChecker);
}
componentWillMount() {
this.tweetChecker = setInterval( … );
}
componentWillUnmount() {
clearInterval(this.tweetChecker);
}
componentDidMount() {
Api.getAll('/api/items').then(items !=> {
this.setState({ items });
});
}
componentDidMount() {
Api.getAll('/api/items').then(items !=> {
this.setState({ items });
});
}
• We’ll do it together.
render() {
const { count } = this.state;
return (
<div>
<Counter count={count} onIncrement={this.increment} !/>
<Counter count={count} onIncrement={this.increment} !/>
!</div>
);
}
}
Live Coding
Exercise
• https://github.com/stevekinney/pizza-calculator
• You joke, but this is an application that got a lot of use at my old
gig.
render() {
return (
<WrappedComponent
count={this.state.count}
onIncrement={this.increment}
{!!...this.props}
!/>
);
}
};
const CounterWithCount = WithCount(Counter);
Live Coding
Exercise
• So, I’m ordering pizzas for two different events apparently and
I can’t be bothered to open two tabs.
• Can you use the Higher Order Component pattern to create two
separately managed calculators?
I love the Higher Order
Component pattern, but…
Sometimes, it’s hard to see
what’s going on inside.
I also don’t want to make two of every
component: a presentational and a
presentation wrapped in a container.
Render Properties
export default class WithCount extends Component {
state = { count: 0 };
increment = () !=> {
this.setState(state !=> ({ count: state.count + 1 }));
};
render() {
return (
<div className="WithCount">
{
this.props.render(
this.state.count,
this.increment,
)
}
!</div>
);
}
}
<WithCount render={
(count, increment) !=> (
<Counter count={count} onIncrement={increment} !/>
)
}
!/>
Live Coding
Exercise
• I don’t have a good story for why I’m going to make you do
this.
AppDispatcher.register(action !=> {
!// !!...
});
}
getItems() {
return items;
}
addItem(item) {
items = [!!...items, item];
this.emit('change');
}
}
Your views listen for these
change events.
class Application extends Component {
state = {
items: ItemStore.getItems(),
};
updateItems = () !=> {
const items = ItemStore.getItems();
this.setState({ items });
};
componentDidMount() {
ItemStore.on('change', this.updateItems);
}
componentWillUnmount() {
ItemStore.off('change', this.updateItems);
}
render() { … }
}
Your views can trigger actions
based on user interaction.
import { addItem } from '!../actions';
handleChange(event) { … }
handleSubmit(event) {
const { value } = this.state;
event.preventDefault();
render() { … }
}
Which then goes to the dispatcher
and then to the store, which triggers
another change—updating the view.
Exercise
reduce(state, action) {
if (action.type !!=== 'ADD_NEW_ITEM') {
return [ !!...state.items, action.item ]
}
return state;
}
}
Part Six
Redux and React
We’re going to do that thing again.
• Then I’m going to explain the moving pieces once you’ve seen
it in action.
React State vs. Redux State
Should I put stuff in the
Redux store?
😩 It depends. 😩
Like most things with
computers—it’s a trade off.
The cost? Indirection.
class NewItem extends Component {
state = { value: '' };
event.preventDefault();
render() { … }
}
Now, it will be in four files!
• NewItem.js
• NewItemContainer.js
• new-item-actions.js
• items-reducer.js
this.setState() is inherently
simpler to reason about than
actions, reducers, and stores.
Think about short-term view state
versus long-term model state.
react-redux
Good news! The react-redux
library is also super small.
Even smaller than Redux!
<Provider !/>
connect()
(Yea, that’s it.)
connect();
It’s the higher-order component pattern!
connect(mapStateToProps, mapDispatchToProps)(WrappedComponent);
Mix these two together and pass them as props to a presentational component.
This is a function that you make that takes the entire state
tree and boils it down to just what your components needs.
render() {
return Children.only(this.props.children)
}
}
class Provider extends Component {…}
Provider.childContextTypes = {
store: PropTypes.shape({
subscribe: PropTypes.func.isRequired,
dispatch: PropTypes.func.isRequired,
getState: PropTypes.func.isRequired,
}),
storeSubscription: PropTypes.shape({
trySubscribe: PropTypes.func.isRequired,
tryUnsubscribe: PropTypes.func.isRequired,
notifyNestedSubs: PropTypes.func.isRequired,
isSubscribed: PropTypes.func.isRequired,
})
};
class SomePresentationalComponent extends Component {
render() { … }
}
SomePresentationalComponent.contextTypes = {
store: PropTypes.shape({
subscribe: PropTypes.func.isRequired,
dispatch: PropTypes.func.isRequired,
getState: PropTypes.func.isRequired,
})
};
Exercise
• Previously, we wired up our actions, reducers, and the store.
• You may not get it all done. Start with the either the packed or
unpacked list. Put some items in the initial state if that helps.
Part Seven
Redux Thunk
Thunk?
thunk (noun): a function
returned from another function.
function definitelyNotAThunk() {
return function aThunk() {
console.log('Hello, I am a thunk.')
}
}
But, why is this useful?
The major idea behind a thunk is
that it’s code to be executed later.
We’ve been a bit quiet about
asynchronous code.
Here is the thing with Redux—it
only accepts objects as actions.
But, we might not have the data ready yet.
• I’ll implement fetching all of the items and adding a new item
from the “server.”
fetchMock.getOnce('/items', {
body: itemsInDatabase,
headers: { 'content-type': 'application/json' },
});
!// Import a file with saga that you made (or, will make).
import saga from './saga';
function* logActionsAndState() {
yield takeEvery('*', function* logger(action) {
const state = yield select();
console.log('action', action);
console.log('state after', state);
});
}
function* cancellableRequest() {
while (true) {
yield take(‘REQUEST_LYFT');
yield race({
task: call(Api.makeRequest),
cancel: take('CANCEL_LYFT')
});
}
}
Part Nine
MobX
An Aside: Computed
Properties
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
const person = new Person('Grace', 'Hopper');
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
const person = new Person('Grace', 'Hopper');
Object.defineProperty(Person.prototype, 'fullName', {
get: function () {
return this.firstName + ' ' + this.lastName;
}
});
An Aside: Decorators
Effectively decorators provide a
syntactic sugar for higher-order
functions.
Target Key
Object.defineProperty(Person.prototype, 'fullName', {
enumerable: false,
writable: false,
Descriptor get: function () {
return this.firstName + ' ' + this.lastName;
}
});
function decoratorName(target, key, descriptor) {
!// …
}
function readonly(target, key, descriptor) {
descriptor.writable = false;
return descriptor;
}
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@autobind
@deprecate
@readonly
@memoize
@debounce
@profile
Okay, so… MobX
Imagine if you could simply
change the state tree.
Bear with me here.
A primary tenet of using MobX is that you can
store state in a simple data structure and allow
the library to care of keeping everything up to
date.
http://bit.ly/super-basic-mobx
Ridiculously simplified, not real code™
const onChange = (oldValue, newValue) !=> {
!// Tell MobX that this value has changed.
}
constructor(firstName, lastName) {
this.firstName;
this.lastName;
}
}
…is effectively equivalent.
function Person (firstName, lastName) {
this.firstName;
this.lastName;
extendObservable(this, {
firstName: firstName,
lastName: lastName
});
}
const extendObservable = (target, source) !=> {
source.keys().forEach(key !=> {
const wrappedInObservable = observable(source[key]);
Object.defineProperty(target, key, {
set: value.set.
get: value.get
});
});
};
!// This is the @observable decorator
const observable = (object) !=> {
return extendObservable(object, object);
};
Four-ish major concepts
• Observable state
• Actions
• Derivations
• Computed properties
• Reactions
Computed properties update their
value based on observable data.
Reactions produce side
effects.
class PizzaCalculator {
numberOfPeople = 0;
slicesPerPerson = 2;
slicesPerPie = 8;
get slicesNeeded() {
return this.numberOfPeople * this.slicesPerPerson;
}
get piesNeeded() {
return Math.ceil(this.slicesNeeded / this.slicesPerPie);
}
addGuest() { this.numberOfPeople!++; }
}
import { action, observable, computed } from 'mobx';
class PizzaCalculator {
@observable numberOfPeople = 0;
@observable slicesPerPerson = 2;
@observable slicesPerPie = 8;
@action addGuest() {
this.numberOfPeople!++;
}
}
You may notice that you can
sometimes omit the @action
annotation.
Originally, this was just for
documenting, but we’ll see that it has
some special implications now-a-days.
You can also pass most common data structures to
MobX.
• Objects — observable({})
• Arrays — observable([])
componentWillUnmount() {
this.stopListening();
}
render() { … }
}
import { Provider } from 'mobx-react';
ReactDOM.render(
<Provider itemStore={itemStore}>
<Application !/>
!</Provider>,
document.getElementById('root'),
);
@inject('itemStore')
class NewItem extends Component {
state = { … };
render() { … }
}
const UnpackedItems = inject('itemStore')(
observer(({ itemStore }) !=> (
<Items
title="Unpacked Items"
items={itemStore.filteredUnpackedItems}
total={itemStore.unpackedItemsLength}
>
<Filter
value={itemStore.unpackedItemsFilter}
onChange={itemStore.updateUnpackedItemsFilter}
!/>
!</Items>
)),
);
Exercise
• I’ll implement the basic functionality for adding and removing
items.
• This time it will be the same flow as last time, but we’re going
to add asynchronous calls to the server into the mix.
Conclusion
Closing Thoughts
MobX versus Redux
MobX versus Redux
Dependency Graphs versus Immutable
State Trees
Advantages of Dependency Graphs
• Easy to update
• Reference by identity
Advantages of Immutable State Trees
• Reference by state
What does it mean to have to
normalize your data?
state = {
items: [
{ id: 1, value: "Storm Trooper action figure", owner: 2 },
{ id: 2, value: "Yoga mat", owner: 1 },
{ id: 4, value: "MacBook", owner: 3 },
{ id: 5, value: "iPhone", owner: 1 },
{ id: 7, value: "Melatonin", owner: 3 }
],
owners: [
{ id: 1, name: "Logan", items: [2, 5] },
{ id: 2, name: "Wes", items: [1] },
{ id: 3, name: "Steve", items: [4, 7] }
]
}
state = {
items: {
1: { id: 1, value: "Storm Trooper action figure", owner: 2 },
2: { id: 2, value: "Yoga mat", owner: 1 },
4: { id: 4, value: "MacBook", owner: 3 },
5: { id: 5, value: "iPhone", owner: 1 },
7: { id: 7, value: "Melatonin", owner: 3 }
},
owners: {
1: { id: 1, name: "Logan", items: [2, 5] },
2: { id: 2, name: "Wes", items: [1] },
3: { id: 3, name: "Steve", items: [4, 7] }
}
}
Where can you take this from
here?
Could you implement the undo/
redo pattern outside of Redux?
Would an action/reducer
pattern be helpful in MobX?
Would async/await make a
suitable replacement for thunks or
sagas?
Can you implement undo
with API requests?
You now have a good sense
of the lay of the land.
But, even people who have some
experience are still figuring this
stuff out.
This is not a solved problem.
Welcome to the team.
Fin.