Managing Data with React and Redux 1 @Jack_Franklin @pusher 2 - - PowerPoint PPT Presentation

managing data with react and redux
SMART_READER_LITE
LIVE PREVIEW

Managing Data with React and Redux 1 @Jack_Franklin @pusher 2 - - PowerPoint PPT Presentation

Managing Data with React and Redux 1 @Jack_Franklin @pusher 2 Code, notes, etc: github.com/jackfranklin/ react-redux-talk Slides (after talk): speakerdeck.com/ jackfranklin I'll tweet them all: twitter.com/jack_franklin 3 4 5


slide-1
SLIDE 1

Managing Data with React and Redux

1

slide-2
SLIDE 2

@Jack_Franklin @pusher

2

slide-3
SLIDE 3
  • Code, notes, etc: github.com/jackfranklin/

react-redux-talk

  • Slides (after talk): speakerdeck.com/

jackfranklin

  • I'll tweet them all: twitter.com/jack_franklin

3

slide-4
SLIDE 4

4

slide-5
SLIDE 5

5

slide-6
SLIDE 6

In the beginning

6

slide-7
SLIDE 7

app/todos.js

export default class Todos extends React.Component { constructor(props) { super(props); this.state = { todos: [ { id: 1, name: 'Write the blog post', done: false }, { id: 2, name: 'Buy Christmas presents', done: false }, { id: 3, name: 'Leave Santa his mince pies', done: false }, ] } } ... }

7

slide-8
SLIDE 8

app/todos.js

render() { return ( <div> <p>The <em>best</em> todo app out there.</p> <h1>Things to get done:</h1> <ul className="todos-list">{ this.renderTodos() }</ul> <AddTodo onNewTodo={(todo) => this.addTodo(todo)} /> </div> ) }

8

slide-9
SLIDE 9

<AddTodo onNewTodo={(todo) => this.addTodo(todo)} />

9

slide-10
SLIDE 10

Parent component contains all state. Child components are given functions to call to tell the parent component of the new state.

10

slide-11
SLIDE 11

app/todos.js contained the logic for updating the state from some user input.

constructor(props) {...} addTodo(todo) { const newTodos = this.state.todos.concat([todo]); this.setState({ todos: newTodos }); } ... render() {...}

11

slide-12
SLIDE 12

But then as this component grew I pulled out the business logic into standalone JavaScript functions:

12

slide-13
SLIDE 13

app/todos.js constructor(props) {...} addTodo(todo) { this.setState(addTodo(this.state, todo)); } ... render() {...}

13

slide-14
SLIDE 14

State functions can take the current state and produce a new state.

export function deleteTodo(state, id) { return { todos: state.todos.filter((todo) => todo.id !== id) }; }

14

slide-15
SLIDE 15

This is effectively a very, very basic Redux (but worse in many ways!).

15

slide-16
SLIDE 16

This is fine for small applications, but it tightly couples components and makes refactoring

  • r restructuring components trick and makes

refactoring or restructuring components tricky.

16

slide-17
SLIDE 17

The more data you have, the more difficult it is to manage as different components can edit different pieces of data.

17

slide-18
SLIDE 18

If you split the data up across components, you no longer have a single source of truth for your application's data.

18

slide-19
SLIDE 19

It's tricky to track down what caused the data to change, and where it happened.

grep setState

19

slide-20
SLIDE 20

As your application grows you need some process and structure around your data.

20

slide-21
SLIDE 21

But don't use Redux by default! For smaller apps you'll probably find yourself quite content without.

21

slide-22
SLIDE 22

Redux

22

slide-23
SLIDE 23

The three principles of Redux.

  • Single Source of Truth: all data is stored in
  • ne object.
  • State is read only: nothing can directly mutate

the state.

  • The state is manipulated by pure functions: no

external data can affect them.

23

slide-24
SLIDE 24

Building a Redux application

24

slide-25
SLIDE 25

import { createStore } from 'redux'; function counter(state, action) { ... } const store = createStore(counter); console.log('Current state', store.getState());

25

slide-26
SLIDE 26
  • store: the object that holds our state
  • action: an object sent to the store in order to

manipulate the store's data

  • reducer: a function that takes the current

state, an action and produces the new state.

26

slide-27
SLIDE 27

First: define your actions.

{ type: 'INCREMENT' } { type: 'DECREMENT' }

27

slide-28
SLIDE 28

Second: define how your reducer should deal with those actions:

function counter(state, action) { switch (action.type) { case 'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } }

28

slide-29
SLIDE 29

Third: define what to do if you don't have any state:

function counter(state, action) { if (!state) state = 0; ... } // OR function counter(state = 0, action) { ... }

29

slide-30
SLIDE 30

Fourth: create your store and dispatch some actions:

const store = createStore(counter); store.dispatch({ type: 'INCREMENT' }); console.log('Current state', store.getState()); // => 1

30

slide-31
SLIDE 31

What makes this good?

  • The main logic is contained in a function,

abstracted away from the store. It's easy to modify, follow and test.

  • Nothing ever manipulates the state, all

manipulation is done via actions.

  • Actions are just plain objects; they can be

logged, serialised, repeated, and so on.

  • Our reducer is pure - the state is never

31

slide-32
SLIDE 32

Adding Redux to a React application

npm install --save redux react-redux

32

slide-33
SLIDE 33

First, let's decide what our state will look like:

{ todos: [ { id: 1, name: 'buy milk', done: false }, ... ] }

33

slide-34
SLIDE 34

Secondly, let's define the actions.

  • { type: 'ADD_TODO', name: '...' }
  • { type: 'DELETE_TODO', id: ... }
  • { type: 'TOGGLE_TODO', id: ... }

34

slide-35
SLIDE 35

Thirdly, let's define the reducer function that will deal with these actions.

export default function todoAppReducers( state = { todos: [] }, action ) { ... };

35

slide-36
SLIDE 36

We can first deal with ADD_TODO:

switch (action.type) { case 'ADD_TODO': const todo = { name: action.name, id: state.todos.length, done: false } return { todos: state.todos.concat([todo]) } }

36

slide-37
SLIDE 37

And then DELETE_TODO:

case 'DELETE_TODO': return { todos: state.todos.filter((todo) => todo.id !== action.id) }

37

slide-38
SLIDE 38

And finally TOGGLE_TODO:

case 'TOGGLE_TODO': const todos = state.todos.map((todo) => { if (todo.id === action.id) { todo.done = !todo.done; } return todo; }); return { todos };

38

slide-39
SLIDE 39

We've just modelled most of our business logic without having to deal with UI interactions or anything else. This is one of the biggest pluses to using Redux.

39

slide-40
SLIDE 40

Now let's create a store and connect our components.

40

slide-41
SLIDE 41

By default a component does not have access to the store. Components that do are known as "smart" components. Components that do not are known as "dumb" components.

41

slide-42
SLIDE 42

app/index.js

import React from 'react'; import { render } from 'react-dom'; import Todos from './todos'; class AppComponent extends React.Component { render() { return <Todos />; } } render( <AppComponent />, document.getElementById('app') );

42

slide-43
SLIDE 43

We'll firstly create a store and connect our application.

// other imports skipped import { Provider } from 'react-redux'; import { createStore } from 'redux'; import todoAppReducers from './reducers'; const store = createStore(todoAppReducers); class AppComponent extends React.Component {...} render( <Provider store={store}> <AppComponent /> </Provider>, document.getElementById('app') );

43

slide-44
SLIDE 44

All the Provider does is make components in our application able to connect to the store if we give them permission. You only need to wrap your top level component in the Provider, and once it's done you can mostly forget about it.

44

slide-45
SLIDE 45

We now have a store and our top level component has been given access to it. Now we need to give our components access to the store so they can render the data to it.

45

slide-46
SLIDE 46

The Todos component needs access to the todos in the store so it can render the individual todos. We can use the connect function from react-redux to do this.

46

slide-47
SLIDE 47

app/todos.js import { connect } from 'react-redux'; class Todos extends React.Components {...}; const ConnectedTodos = connect((state) => { return { todos: state.todos } })(Todos);

47

slide-48
SLIDE 48

The connect function takes a React component and produces a new component that will be given access to parts of the state as props. This lets us strictly control which parts of our state each component has access to. It also provides this.props.dispatch, which is used to dispatch actions, which we'll see later.

48

slide-49
SLIDE 49

So now within our Todos component we can swap this.state.todos to this.props.todos. We can also get rid of all the functions for managing state, and stop passing them through to child components.

49

slide-50
SLIDE 50

class Todos extends React.Component { renderTodos() { return this.props.todos.map((todo) => { return <li key={todo.id}><Todo todo={todo} /></li>; }); } render() { return ( <div> <ul className="todos-list">{ this.renderTodos() }</ul> <AddTodo /> </div> ) } }

50

slide-51
SLIDE 51

Notice how much cleaner this is, and how our component is purely focused on presentation.

51

slide-52
SLIDE 52

Now let's hook up AddTodo so we can create new todos.

52

slide-53
SLIDE 53

AddTodo doesn't need to access any data in the store but it does need to dispatch actions, so it too must be connected.

class AddTodo extends React.Component {...}; const ConnectedAddTodo = connect()(AddTodo); export default ConnectedAddTodo;

53

slide-54
SLIDE 54

The old addTodo method:

addTodo(e) { e.preventDefault(); const newTodoName = this.refs.todoTitle.value; if (newTodoName) { this.props.onNewTodo({ name: newTodoName }); this.refs.todoTitle.value = ''; } }

54

slide-55
SLIDE 55

The new one:

addTodo(e) { e.preventDefault(); const newTodoName = this.refs.todoTitle.value; if (newTodoName) { this.props.dispatch({ name: newTodoName, type: 'ADD_TODO' }); ... } }

55

slide-56
SLIDE 56

And we're now using Redux to add Todos!

56

slide-57
SLIDE 57

Finally, we can update the Todo component to dispatch the right actions for toggling and deleting.

57

slide-58
SLIDE 58

toggleDone() { this.props.dispatch({ type: 'TOGGLE_TODO', id: this.props.todo.id }); } deleteTodo(e) { this.props.dispatch({ type: 'DELETE_TODO', id: this.props.todo.id }); }

58

slide-59
SLIDE 59

But there's a problem!

59

slide-60
SLIDE 60

Mutation!

60

slide-61
SLIDE 61

Redux expects you to never mutate anything, and if you do it can't always correctly keep your UI in sync with the state. We've accidentally mutated...

61

slide-62
SLIDE 62

In our reducer...

case 'TOGGLE_TODO': const todos = state.todos.map((todo) => { if (todo.id === action.id) { todo.done = !todo.done; } return todo; }); return { todos };

62

slide-63
SLIDE 63

todo.done = !todo.done;

63

slide-64
SLIDE 64

A quick rewrite...

case 'TOGGLE_TODO': const todos = state.todos.map((todo) => { if (todo.id === action.id) { return { name: todo.name, id: todo.id, done: !todo.done } } return todo; });

64

slide-65
SLIDE 65

And it all works, as does deleting a todo. We're fully Reduxed!

65

slide-66
SLIDE 66

Deep breath...

66

slide-67
SLIDE 67

That probably felt like a lot of effort, but the good news is once you've set Redux up you are set.

67

slide-68
SLIDE 68

1.Decide the shape of your state. 2.Decide the actions that can update the state. 3.Define your reducers that deal with actions. 4.Wire up your UI to dispatch actions. 5.Connect your components to the store to allow them to render state.

68

slide-69
SLIDE 69

Still to come

1.Debugging Redux 2.Better Redux Reducers 3.Middlewares 4.Async actions

69

slide-70
SLIDE 70

70

slide-71
SLIDE 71

If you're not using Chrome you can still use the devtools but it takes a bit more effort. See: https://github.com/gaearon/redux-devtools

71

slide-72
SLIDE 72

Firstly, install the plugin in Chrome.

72

slide-73
SLIDE 73

Secondly, update the call to createStore:

createStore(reducers, initialState, enhancer).

Enhancer: a function that enhances the store with middleware or additional functionality. We'll see this again later.

73

slide-74
SLIDE 74

const store = createStore( todoAppReducers, undefined, window.devToolsExtension ? window.devToolsExtension() : undefined );

We leave the initialState as undefined because our reducer deals with no state.

74

slide-75
SLIDE 75

75

slide-76
SLIDE 76

76

slide-77
SLIDE 77

77

slide-78
SLIDE 78

Better Reducers

78

slide-79
SLIDE 79

Imagine our TODO app now needs to have a user log in first, and our state will now keep track of the user that's logged in.

79

slide-80
SLIDE 80

Our new state will look like:

{ todos: [ { id: 1, name: 'buy milk', done: false }, ... ], user: { id: 123, name: 'Jack' } }

80

slide-81
SLIDE 81

Next, let's define some new actions:

{ type: 'LOG_USER_IN', id: ..., name: '...' } { type: 'LOG_USER_OUT' }

81

slide-82
SLIDE 82

And then our reducer needs to be updated. Firstly, now we have two keys in our state, we should update the reducers we have to not lose any keys they don't deal with.

82

slide-83
SLIDE 83

Before:

case 'DELETE_TODO': return { todos: state.todos.filter((todo) => todo.id !== action.id) }

After:

case 'DELETE_TODO': return Object.assign({}, state, { todos: state.todos.filter((todo) => todo.id !== action.id) });

83

slide-84
SLIDE 84

And now we can add reducers for the new user actions:

case 'LOG_USER_IN': return Object.assign({}, state, { user: { id: action.id, name: action.name } }); case 'LOG_USER_OUT': return Object.assign({}, state, { user: {} });

84

slide-85
SLIDE 85 export default function todoAppReducers( state = { todos: [initialTodo], user: {} }, action ) { switch (action.type) { case 'ADD_TODO': const todo = { name: action.name, id: state.todos.length, done: false } return Object.assign({}, state, { todos: state.todos.concat([todo]) }); case 'DELETE_TODO': return Object.assign({}, state, { todos: state.todos.filter((todo) => todo.id !== action.id) }); case 'TOGGLE_TODO': const todos = state.todos.map((todo) => { if (todo.id === action.id) { return { name: todo.name, id: todo.id, done: !todo.done } } return todo; }); return Object.assign({}, state, { todos }); case 'LOG_USER_IN': return Object.assign({}, state, { user: { id: action.id, name: action.name } }); case 'LOG_USER_OUT': return Object.assign({}, state, { user: {} }); default: return state; } };

85

slide-86
SLIDE 86

Our reducer is huge, and deals with two different data sets:

  • User
  • Todos

In a larger app this will quickly become impossible to manage.

86

slide-87
SLIDE 87

Instead we split into multiple reducers who are each resopnsible for a specific key in the state. Each of these reducers is only given their part of the state.

87

slide-88
SLIDE 88

function userReducer(user = {}, action) { switch (action.type) { case 'LOG_USER_IN': return { id: action.id, name: action.name } case 'LOG_USER_OUT': return {}; default: return user; } }

88

slide-89
SLIDE 89

function todoReducer(todos = [], action) { switch (action.type) { case 'ADD_TODO': ... case 'DELETE_TODO': return todos.filter((todo) => todo.id !== action.id); case 'TOGGLE_TODO': ... default: return todos; } }

89

slide-90
SLIDE 90

And our main reducer function becomes:

export default function todoAppReducers(state = {}, action) { return { todos: todoReducer(state.todos, action), user: userReducer(state.user, action) } };

90

slide-91
SLIDE 91

Now as our state grows we'll add new functions for each key. Turns out this pattern is so useful that Redux provides a method to do it for us: combineReducers.

91

slide-92
SLIDE 92

Before:

export default function todoAppReducers(state = {}, action) { return { todos: todoReducer(state.todos, action), user: userReducer(state.user, action) } };

92

slide-93
SLIDE 93

After: import { combineReducers } from 'redux'; ... const todoAppReducers = combineReducers({ todos: todoReducer, user: userReducer }); export default todoAppReducers;

93

slide-94
SLIDE 94

Deep breath again!

94

slide-95
SLIDE 95

Middlewares

95

slide-96
SLIDE 96

You might be familiar with Rack Middlewares, NodeJS / Express middlewares, and so on. It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer

  • - http://redux.js.org/docs/advanced/

Middleware.html

96

slide-97
SLIDE 97

A middleware provides a function that is given the store object. It should return another function that is called with next, the function it should call when it is finished. That function should return another function that is called with the current action, action.

97

slide-98
SLIDE 98

WHAT?!

98

slide-99
SLIDE 99

const myMiddleware = function(store) { return function(next) { return function(action) { // your logic goes here // this is the dispatch function // that each middleware can return } } } const myMiddleware = store => next => action => { // your logic here }

99

slide-100
SLIDE 100

Each middleware replaces the dispatch function with its own function, and Redux chains them together correctly.

100

slide-101
SLIDE 101

Example: logging each action

101

slide-102
SLIDE 102

app/middlewares.js

export const logMiddleware = store => next => action => { console.log('MIDDLEWARE: About to dispatch', action); return next(action); };

102

slide-103
SLIDE 103

app/index.js

import { createStore, applyMiddleware } from 'redux'; ... const store = createStore( todoAppReducers, undefined, applyMiddleware(logMiddleware) );

103

slide-104
SLIDE 104

104

slide-105
SLIDE 105

But wait, we lost the devtools code!

105

slide-106
SLIDE 106

compose

106

slide-107
SLIDE 107

const store = createStore( todoAppReducers, undefined, compose( applyMiddleware(logMiddleware), window.devToolsExtension ? window.devToolsExtension() : f => f ) );

107

slide-108
SLIDE 108

Final deep breath!

108

slide-109
SLIDE 109

Async Actions

109

slide-110
SLIDE 110

Up until now we've dealt purely with synchronous actions, but often your action will be async, most commonly, data fetching.

110

slide-111
SLIDE 111

We could write our own middleware or logic to deal with this, but one already exists: redux-thunk.

npm install --save redux-thunk

111

slide-112
SLIDE 112

With thunk, functions can either return an action

  • r another function that can dispatch actions.

112

slide-113
SLIDE 113

The HTTP request cycle:

  • Make the request, and dispatch action.
  • Get the data back, and dispatch an action with

that data.

  • Request errors, dispatch an action with

information about the error. When you want to dispatch an action to cause an HTTP request you actually want to trigger multiple actions.

113

slide-114
SLIDE 114

export default function thunkMiddleware({ dispatch, getState }) { return next => action => { if (typeof action === 'function') { return action(dispatch, getState); } return next(action); }; }

114

slide-115
SLIDE 115

Fake API:

export function fetchTodos() { return new Promise((resolve, reject) => { resolve({ todos: [{ id: 1, name: 'Buy Milk', done: false }] }) }); }

115

slide-116
SLIDE 116

We need some new information on the state:

{ todos: ..., user: ..., isFetching: true / false }

116

slide-117
SLIDE 117

And some new actions:

{ type: 'REQUEST_TODOS_INIT' }, { type: 'REQUEST_TODOS_SUCCESS', todos: [...] } // and one for error handling // if this were real

117

slide-118
SLIDE 118

function isFetchingReducer(isFetching = false, action) { switch (action.type) { case 'REQUEST_TODOS_INIT': return true; case 'REQUEST_TODOS_SUCCESS': return false default: return isFetching } }

118

slide-119
SLIDE 119

const todoAppReducers = combineReducers({ todos: todoReducer, user: userReducer, isFetching: isFetchingReducer });

119

slide-120
SLIDE 120

Action Creators

app/action-creators.js

export function fetchTodosAction() { return (dispatch) => { } }

120

slide-121
SLIDE 121

app/todos.js

import { fetchTodosAction } from './action-creators'; class Todos extends React.Component { componentWillMount() { this.props.dispatch(fetchTodosAction()); } ... }

121

slide-122
SLIDE 122

First, dispatch REQUEST_TODOS_INIT:

export function fetchTodosAction() { return (dispatch) => { dispatch({ type: 'REQUEST_TODOS_INIT' }); } }

122

slide-123
SLIDE 123

Then, fetch and dispatch REQUEST_TODOS_SUCCESS:

fetchTodos().then((data) => { dispatch({ type: 'REQUEST_TODOS_SUCCESS', todos: data.todos }); });

123

slide-124
SLIDE 124

import { fetchTodos } from './fake-api'; export function fetchTodosAction() { return (dispatch) => { dispatch({ type: 'REQUEST_TODOS_INIT' }); fetchTodos().then((data) => { dispatch({ type: 'REQUEST_TODOS_SUCCESS', todos: data.todos }); }); } }

124

slide-125
SLIDE 125

app/todos.js Within render:

{ this.props.isFetching && <p>LOADING...</p> }

Allow it access:

const ConnectedTodos = connect((state) => { return { todos: state.todos, isFetching: state.isFetching }; })(Todos);

125

slide-126
SLIDE 126

Now we need our todosReducer to deal with the success action and use the data.

case 'REQUEST_TODOS_SUCCESS': return action.todos;

126

slide-127
SLIDE 127

Et voila (the delay is me for effect!):

127

slide-128
SLIDE 128

And with that we now have support for async!

128

slide-129
SLIDE 129

Housekeeping Actions

129

slide-130
SLIDE 130

The strings for our actions are cropping up in a lot of places. Better to have them as constants that can be exported.

export const LOG_USER_IN = 'LOG_USER_IN'; export const LOG_USER_OUT = 'LOG_USER_OUT';

That way you can keep things in sync easier.

130

slide-131
SLIDE 131

Use action creators for creating actions:

export function addTodo(name) { return { type: ADD_TODO, name } };

131

slide-132
SLIDE 132

For more info:

  • http://redux.js.org/docs/basics/Actions.html
  • http://redux.js.org/docs/recipes/

ReducingBoilerplate.html

132

slide-133
SLIDE 133

You've made it!

Redux is tough to get started with but the benefits in a large application are huge.

  • Code, notes, etc: github.com/jackfranklin/

react-redux-talk

  • Slides (after talk): speakerdeck.com/

jackfranklin

  • I'll tweet them all: twitter.com/jack_franklin

133

slide-134
SLIDE 134

Thanks :)

  • @Jack_Franklin
  • javascriptplayground.com

134

slide-135
SLIDE 135

135