Slide 1

Slide 1 text

State Management in React, v2 Steve Kinney A Frontend Masters Workshop

Slide 2

Slide 2 text

Hi, I’m Steve. (@stevekinney)

Slide 3

Slide 3 text

This a course for keeping your state manageable when it’s no longer a toy application.

Slide 4

Slide 4 text

In this course, we’ll be working with pure React.

Slide 5

Slide 5 text

So, what are we going to do today? • Think deeply about what “state” even means in a React application. • Learn a bit about the inner workings of this.setState. • How class-based component state and hooks differ. • Explore APIs for navigating around prop-drilling. • Use reducers for advanced state management.

Slide 6

Slide 6 text

So, what are we going to do today? • Write our own custom hooks for managing state. • Store state in Local Storage. • Store state in the URL using query parameters. • Fetch state from a server—because that’s a thing.

Slide 7

Slide 7 text

And now… Understanding State

Slide 8

Slide 8 text

The main job of React is to take your application state and turn it into DOM nodes.

Slide 9

Slide 9 text

There are many kinds of state. • Model data: The nouns in your application. • View/UI state: Are those nouns sorted in ascending or descending order? • Session state: Is the user even logged in? • Communication: Are we in the process of fetching the nouns from the server? • Location: Where are we in the application? Which nouns are we looking at?

Slide 10

Slide 10 text

Or, it might make sense to think about state relative to time. • Model state: This is likely the data in your application. This could be the items in a given list. • Ephemeral state: Stuff like the value of an input field that will be wiped away when you hit “enter.” This could be the order in which a given list is sorted.

Slide 11

Slide 11 text

Spoiler alert: There is no silver bullet.

Slide 12

Slide 12 text

And now… An Uncomfortably Close Look at React Component State

Slide 13

Slide 13 text

Let’s start with the world’s simplest React component.

Slide 14

Slide 14 text

Exercise • Okay, this is going to be a quick one to get warmed up. • https://github.com/stevekinney/simple-counter-react-state • It will have three buttons: • Increment • Decrement • Reset • Your job: Get decrement and reset working. • Only touch for now.

Slide 15

Slide 15 text

Oh, wow—it looks like it’s time for a pop quiz, already.

Slide 16

Slide 16 text

class Counter extends Component { constructor() { this.state = { counter: 0 } } render() { … } }

Slide 17

Slide 17 text

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);

Slide 18

Slide 18 text

Any guesses?

Slide 19

Slide 19 text

0

Slide 20

Slide 20 text

this.setState() is asynchronous.

Slide 21

Slide 21 text

React is trying to avoid unnecessary re-renders.

Slide 22

Slide 22 text

export default class Counter extends Component { constructor() { super(); this.state = { count: 0 }; this.increment = this.increment.bind(this); } increment() {…} render() { return (

Count: {this.state.count}!

Increment! ! ) } }

Slide 23

Slide 23 text

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() { … } }

Slide 24

Slide 24 text

What will the count be after the user’s clicks the “Increment” button?

Slide 25

Slide 25 text

1

Slide 26

Slide 26 text

Effectively, you’re queuing up state changes.

Slide 27

Slide 27 text

React will batch them up, figure out the result and then efficiently make that change.

Slide 28

Slide 28 text

Object.assign( {}, yourFirstCallToSetState, yourSecondCallToSetState, yourThirdCallToSetState, );

Slide 29

Slide 29 text

const newState = { !!...yourFirstCallToSetState, !!...yourSecondCallToSetState, !!...yourThirdCallToSetState, };

Slide 30

Slide 30 text

There is actually a bit more to this.setState().

Slide 31

Slide 31 text

Fun fact: Did you know that you can also pass a function in as an argument?

Slide 32

Slide 32 text

import React, { Component } from 'react'; export default class Counter extends Component { constructor() { … } 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() { … } }

Slide 33

Slide 33 text

3

Slide 34

Slide 34 text

increment() { this.setState(state !=> { return { count: state.count + 1 }; }); }

Slide 35

Slide 35 text

increment() { this.setState(({ count }) !=> { return { count: count + 1 }; }); }

Slide 36

Slide 36 text

When you pass functions to this.setState(), it plays through each of them.

Slide 37

Slide 37 text

import React, { Component } from 'react'; export default class Counter extends Component { constructor() { … } increment() { this.setState(state !=> { if (state.count !>= 5) return; return { count: state.count + 1 }; }) } render() { … } }

Slide 38

Slide 38 text

Live Coding

Slide 39

Slide 39 text

this.setState also takes a callback.

Slide 40

Slide 40 text

import React, { Component } from 'react'; export default class Counter extends Component { constructor() { … } increment() { this.setState( { count: this.state.count + 1 }, () !=> { console.log(this.state); } ) } render() { … } }

Slide 41

Slide 41 text

Live Coding

Slide 42

Slide 42 text

Patterns and anti-patterns

Slide 43

Slide 43 text

When we’re working with props, we have PropTypes. That’s not the case with state.*

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

Don’t use this.state for derivations of props.

Slide 46

Slide 46 text

class User extends Component { constructor(props) { super(props); this.state = { fullName: props.firstName + ' ' + props.lastName }; } }

Slide 47

Slide 47 text

Don’t do this. Instead, derive computed properties directly from the props themselves.

Slide 48

Slide 48 text

class User extends Component { render() { const { firstName, lastName } = this.props; const fullName = firstName + ' ' + lastName; return (

{fullName}!

); } }

Slide 49

Slide 49 text

!// Alternatively… class User extends Component { get fullName() { const { firstName, lastName } = this.props; return firstName + ' ' + lastName; } render() { return (

{this.fullName}!

); } }

Slide 50

Slide 50 text

You don’t need to shove everything into your render method.

Slide 51

Slide 51 text

You can break things out into helper methods.

Slide 52

Slide 52 text

class UserList extends Component { render() { const { users } = this.props; return ( { users.map(user !=> ( )) } ! ); } }

Slide 53

Slide 53 text

class UserList extends Component { renderUserProfile(user) { return ( ) } render() { const { users } = this.props; return ( { users.map(this.renderUserProfile) } ! ); } }

Slide 54

Slide 54 text

const renderUserProfile = user !=> { return ( ); }; const UserList = ({ users }) !=> { return ( {users.map(renderUserProfile)} ! ); };

Slide 55

Slide 55 text

Don’t use state for things you’re not going to render.

Slide 56

Slide 56 text

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); } render() { !// Do stuff with tweets } }

Slide 57

Slide 57 text

class TweetStream extends Component { constructor() { super(); this.state = { tweets: [], } } componentWillMount() { this.tweetChecker = setInterval( … ); } componentWillUnmount() { clearInterval(this.tweetChecker); } render() { !// Do stuff with tweets } }

Slide 58

Slide 58 text

Use sensible defaults.

Slide 59

Slide 59 text

class Items extends Component { constructor() { super(); } componentDidMount() { Api.getAll('/api/items').then(items !=> { this.setState({ items }); }); } render() { !// Do stuff with items } }

Slide 60

Slide 60 text

class Items extends Component { constructor() { super(); this.state = { items: [] } } componentDidMount() { Api.getAll('/api/items').then(items !=> { this.setState({ items }); }); } render() { !// Do stuff with items } }

Slide 61

Slide 61 text

And now… An Equally Uncomfortably Close Look at React Hooks

Slide 62

Slide 62 text

const [count, setCount] = React.useState(0); const increment = () !=> setCount(count + 1); const decrement = () !=> setCount(count - 1); const reset = () !=> setCount(0);

Slide 63

Slide 63 text

const increment = () !=> { setCount(count + 1); setCount(count + 1); setCount(count + 1); };

Slide 64

Slide 64 text

const increment = () !=> { setCount(c !=> c + 1); };

Slide 65

Slide 65 text

const increment = () !=> { setCount(c !=> c + 1); setCount(c !=> c + 1); setCount(c !=> c + 1); };

Slide 66

Slide 66 text

setCount(c !=> { if (c !>= max) return; return c + 1; });

Slide 67

Slide 67 text

setCount(c !=> { if (c !>= max) return c; return c + 1; });

Slide 68

Slide 68 text

Live Coding

Slide 69

Slide 69 text

Exercise • Can you add a second effect that updates the document’s title whenever the count changes? • (Hint: document.title is your friend here.)

Slide 70

Slide 70 text

Live Coding

Slide 71

Slide 71 text

How do lifecycle methods and hooks differ?

Slide 72

Slide 72 text

componentDidUpdate() { setTimeout(() !=> { console.log(`Count: ${this.state.count}`); }, 3000); }

Slide 73

Slide 73 text

React.useEffect(() !=> { setTimeout(() !=> { console.log(`Count: ${count}`); }, 3000); }, [count]);

Slide 74

Slide 74 text

const countRef = React.useRef(); countRef.current = count; React.useEffect(() !=> { setTimeout(() !=> { console.log(`You clicked ${countRef.current} times`); }, 3000); }, [count]);

Slide 75

Slide 75 text

Live Coding

Slide 76

Slide 76 text

Cleaning Up After useEffect

Slide 77

Slide 77 text

useEffect(() !=> { let x; const id = setInterval(() !=> { console.log(x!++); }, 3000); return () !=> { clearInterval(x!++); } });

Slide 78

Slide 78 text

Live Coding

Slide 79

Slide 79 text

And now… The Joy of useReducer

Slide 80

Slide 80 text

Introducing Grudge List

Slide 81

Slide 81 text

No content

Slide 82

Slide 82 text

No content

Slide 83

Slide 83 text

No content

Slide 84

Slide 84 text

No content

Slide 85

Slide 85 text

No content

Slide 86

Slide 86 text

No content

Slide 87

Slide 87 text

useReducer()

Slide 88

Slide 88 text

What’s the deal with useReducer()? • So, it turns out that it has nothing to do with Redux. • But, it does allow you to use reducers—just like Redux. • The cool part is that it allows you to create interfaces where you (or a friend) can pass in the mechanics about how to update state.

Slide 89

Slide 89 text

Live Coding

Slide 90

Slide 90 text

Exercise • Be a better person than me. • I’ve implemented the ability to add a grudge. • Can you implement the ability to forgive one?

Slide 91

Slide 91 text

And now… The Perils Prop Drilling

Slide 92

Slide 92 text

Prop drilling occurs when you have deep component trees.

Slide 93

Slide 93 text

Application New Grudge Grudges Grudge Grudge Grudge

Slide 94

Slide 94 text

No content

Slide 95

Slide 95 text

Cards Cards Card Card Application Boards New Board Users New User Board Board Board Cards Card Card Card Card User User Move to Board Assign to User Move to Board Assign to User Move to Board Assign to User Move to Board Assign to User Move to Board Assign to User Move to Board Assign to User

Slide 96

Slide 96 text

And now… The Context API

Slide 97

Slide 97 text

“ Context provides a way to pass data through the component tree without having to pass props down manually at every level.

Slide 98

Slide 98 text

React.createContext() Provider Consumer

Slide 99

Slide 99 text

import React from 'react'; const SuperCoolContext = React.createContext(); SuperCoolContext.Provider; SuperCoolContext.Consumer;

Slide 100

Slide 100 text

{ value !=>

{value}!

} ! !

Slide 101

Slide 101 text

const CountContext = createContext(); class CountProvider extends Component { state = { count: 0 }; increment = () !=> this.setState(({ count }) !=> ({ count: count + 1 })); decrement = () !=> this.setState(({ count }) !=> ({ count: count - 1 })); render() { const { increment, decrement } = this; const { count } = this.state; const value = { count, increment, decrement }; return ( {this.props.children} ! ); } }

Slide 102

Slide 102 text

Live Coding

Slide 103

Slide 103 text

Some Tasting Notes • We lost all of our performance optimizations when moving to the Context API. • What’s the right answer? It’s a trade off. • Grudge List might seem like a toy application, but it could also represent a smaller part of a larger system. • Could you use the Context API to get things all of the way down to this level and then use the approach we had previously?

Slide 104

Slide 104 text

And now… How you structure your state matters

Slide 105

Slide 105 text

Some High Level Guidance • Keep your data structures flat. • Prefer objects to arrays.

Slide 106

Slide 106 text

const board = { lists: [ { id: '1558196567543', title: 'Backburner', cards: [ { id: '1558196597470', title: 'Learn to Normalize Data', description: 'Iterating through arrays is rough business', }, !// … ], }, !// … ], };

Slide 107

Slide 107 text

const board = { lists: { '1558196567543': { title: 'Backburner', cards: ['1558196597470'], }, !// … }, cards: { '1558196597470': { title: 'Learn to Normalize State', description: 'This is much better.', assignedTo: '1', }, !// … }, users: { '1': { name: 'Steve Kinney', }, }, };

Slide 108

Slide 108 text

const removeCard = (listId, cardId) !=> { const targetList = lists.find(list !=> listId !!=== list.id); const remainingCards = targetList.cards.filter(({ id }) !=> id !!!== cardId); const updatedList = { !!...targetList, cards: remainingCards }; const updatedLists = lists.map(list !=> { return list.id !!=== listId ? updatedList : list; }); setLists(updatedLists); };

Slide 109

Slide 109 text

const removeCard = (listId, cardId) !=> { const list = this.state.lists[listId]; const cards = omit(this.cards, cardId); const lists = { !!...this.state.lists, [listId]: { !!...list, cards: list.cards.filter(card !=> card.id !!=== cardId), }, }; this.setState({ lists, cards }); };

Slide 110

Slide 110 text

const board = { lists: { '1558196567543': { title: 'Backburner', cards: ['1558196597470'], }, !// … }, cards: { '1558196597470': { title: 'Learn to Normalize State', description: 'This is much better.', assignedTo: '1', }, !// … }, users: { '1': { name: 'Steve Kinney', }, }, };

Slide 111

Slide 111 text

No content

Slide 112

Slide 112 text

Moral of the Story • Being thoughtful about how you structure your state can have implications for the maintainability of your application. • It can also have some implications for performance as well. • Modern versions of React come with tools for eliminating some of the pain points that used to require third party libraries.

Slide 113

Slide 113 text

And now… What about fetching data?

Slide 114

Slide 114 text

useEffect is your friend.

Slide 115

Slide 115 text

Live Coding

Slide 116

Slide 116 text

Exercise • Can you factor this out into a useFetch hook? • It should take an endpoint as an argument. • It should return response, loading, and error. • You might want the following in order to get the right data back out. • const characters = (response !&& response.characters) !|| [];

Slide 117

Slide 117 text

Thunk?

Slide 118

Slide 118 text

thunk (noun): a function returned from another function.

Slide 119

Slide 119 text

function definitelyNotAThunk() { return function aThunk() { console.log('Hello, I am a thunk.') } }

Slide 120

Slide 120 text

But, why is this useful?

Slide 121

Slide 121 text

The major idea behind a thunk is that it is code to be executed later.

Slide 122

Slide 122 text

We’ve been a bit quiet about asynchronous code.

Slide 123

Slide 123 text

Here is the thing with reducers— they only accept objects as actions.

Slide 124

Slide 124 text

export const getAllItems = () !=> ({ type: UPDATE_ALL_ITEMS, items, });

Slide 125

Slide 125 text

export const getAllItems = () !=> { return dispatch !=> { Api.getAll().then(items !=> { dispatch({ type: UPDATE_ALL_ITEMS, items, }); }); }; };

Slide 126

Slide 126 text

Live Coding

Slide 127

Slide 127 text

Exercise • There is another component called CharacterSearch. • It would be cool if we could use the api/search/:query endpoint to get all of the characters that match whatever is typed in that bar. • Can you implement that?

Slide 128

Slide 128 text

And now… Advanced Patterns: Implementing Undo & Redo

Slide 129

Slide 129 text

No content

Slide 130

Slide 130 text

{ past: [allPastStates], present: currentStateOfTheWorld, future: [anyAndAllFutureStates] }

Slide 131

Slide 131 text

And now… Having Our Cake and Implementing It Too: setState Using Hooks

Slide 132

Slide 132 text

Live Coding

Slide 133

Slide 133 text

And now… Using the Route to Manage State: A Brief Treatise

Slide 134

Slide 134 text

Why keep state in the route? • It’s an established pattern that predates most of what we’re doing in the browser these days. • It allows our users to save and share the URL with all of the state baked right in.

Slide 135

Slide 135 text

useQueryParam()

Slide 136

Slide 136 text

https://codesandbox.io/s/ counter-use-query-params-6xpzo

Slide 137

Slide 137 text

And now… The Ecosystem of Third- Party Hooks

Slide 138

Slide 138 text

https://nikgraf.github.io/ react-hooks/

Slide 139

Slide 139 text

Fin.