React and Redux

React and Redux

Redux is FLUX inspired architecture which is becoming de-facto standard for building React applications. But way? The talk tries to explain that.

(Video: https://www.youtube.com/watch?v=zdY1YqjjT6s)

7a0e72a6f55811246bb5d9a946fd2e49?s=128

Radoslav Stankov

March 27, 2016
Tweet

Transcript

  1. React & Redux Radoslav Stankov 27/03/2016

  2. Radoslav Stankov @rstankov http://rstankov.com http://github.com/rstankov

  3. None
  4. None
  5. https://github.com/rstankov/talks-code

  6. None
  7. None
  8. None
  9. React

  10. None
  11. const FILTERS = { All: () => true, Active: (t)

    => !t.completed, Completed: (t) => t.completed, }; export default class App extends React.Component { constructor(props) { super(props); this.state = { filter: 'All', todos: [], }; } render() { return ( <div className="app"> <div className="arrow"> <Input onSave={this.handleNewTodo.bind(this)} /> <ul className="list"> {this.filteredTodos().map((todo) => ( <li key={todo.id} className={todo.completed ? 'completed' : ''}> <input className="toggle" type="checkbox" checked={todo.complete
  12. render() { return ( <div className="app"> <div className="arrow"> <Input onSave={this.handleNewTodo.bind(this)}

    /> <ul className="list"> {this.filteredTodos().map((todo) => ( <li key={todo.id} className={todo.completed ? 'completed' : ''}> <input className="toggle" type="checkbox" checked={todo.complete <label>{todo.text}</label> <button className="destroy" onClick={this.removeTodoHandler(todo </li> ))} </ul> <div className="footer"> <div className="counter">{this.counterText()}</div> {Object.keys(FILTERS).map((filterName) => ( <button className={filterName === this.state.filter ? 'selected' : ))} <button className="clear" style={{visibility: this.showCompletedCoun </div> </div> ); }
  13. counterText() { const count = this.state.todos.filter((t) => !t.completed).length; return `${

    count } ${ count === 1 ? 'item' : 'items' } left`; } filteredTodos() { return this.state.todos.filter(FILTERS[this.state.filter]); } showCompletedCount() { return this.todos.filter((t) => t.completed).length > 0; } handleNewTodo(text) { const todo = { id: +(new Date()), text: text, completed: false }; this.setState({ todos: [todo].concat(this.state.todos), }); }
  14. filteredTodos() { return this.state.todos.filter(FILTERS[this.state.filter]); } showCompletedCount() { return this.todos.filter((t) =>

    t.completed).length > 0; } handleNewTodo(text) { const todo = { id: +(new Date()), text: text, completed: false }; this.setState({ todos: [todo].concat(this.state.todos), }); } handleClearCompleted() { this.setState({ todos: this.state.todos.filter((t) => !t.completed), }); }
  15. this.setState({ todos: [todo].concat(this.state.todos), }); } handleClearCompleted() { this.setState({ todos: this.state.todos.filter((t)

    => !t.completed), }); } removeTodoHandler(todo) { return (e) => { this.setState({ todos: this.state.todos.filter((t) => t.id !== todo.id), }); }; } toggleTodoHandler(todo) { return (e) => { this.setState({ todos: this.state.todos.map((t) => { if (t.id === todo.id) {
  16. }); }; } toggleTodoHandler(todo) { return (e) => { this.setState({

    todos: this.state.todos.map((t) => { if (t.id === todo.id) { t.completed = !t.completed; } return t; }), }); }; } filterHander(filterName) { return (e) => { this.setState({ filter: filterName, }); }; } }
  17. None
  18. None
  19. <App> <NewTodo /> <TodoList> <TodoItem> </TodoList> <Footer> <Filters /> </Footer>

    </App>
  20. <App> <NewTodo /> <TodoList> <TodoItem> </TodoList> <Footer> <Filters /> </Footer>

    </App>
  21. <App> <NewTodo /> <TodoList> <TodoItem> </TodoList> <Footer> <Filters /> </Footer>

    </App>
  22. <App> <NewTodo /> <TodoList> <TodoItem> </TodoList> <Footer> <Filters /> </Footer>

    </App>
  23. <App> <NewTodo /> <TodoList> <TodoItem> </TodoList> <Footer> <Filters /> </Footer>

    </App>
  24. <App> <NewTodo /> <TodoList> <TodoItem> </TodoList> <Footer> <Filters /> </Footer>

    </App>
  25. <App> <NewTodo /> <TodoList> <TodoItem> </TodoList> <Footer> <Filters /> </Footer>

    </App>
  26. <App> <NewTodo /> <TodoList> <TodoItem> </TodoList> <Footer> <Filters /> </Footer>

    </App>
  27. Where is all the data?

  28. <App> [todos] [filter] <NewTodo /> <TodoList> <TodoItem> </TodoList> <Footer> <Filters

    /> </Footer> </App>
  29. export default class App extends React.Component { // . .

    . render() { return ( <div className="app"> <NewTodo addTodo={this.addTodo} /> <TodoList todos={this.state.todos.filter(FILTERS[this.state.filter])} toggleTodo={this.updateTodo} removeTodo={this.removeTodo} /> <Footer todos={this.state.todos} filters={FILTERS} activeFilter={this.state.filter} changeFilter={this.changeFilter} clearCompletedTodos={this.clearCompletedTodos} /> </div> ); } }
  30. export default class TodoList extends React.Component { static propTypes =

    { toggleTodo: React.PropTypes.func.isRequired, removeTodo: React.PropTypes.func.isRequired, todos: React.PropTypes.array.isRequired, }; render() { return ( <ul className="list"> {this.props.todos.map((todo) => ( <TodoItem key={todo.id} todo={todo} toggleTodo={this.props.updateTodo} removeTodo={this.props.removeTodo} /> ))} </ul> ); } }
  31. None
  32. Let’s add new action

  33. <App> [todos] [filter] <NewTodo /> <TodoList> <TodoItem> </TodoList> <Footer> <Filters

    /> </Footer> </App>
  34. export default class App extends React.Component { // . .

    . render() { return ( <div className="app"> <NewTodo addTodo={this.addTodo} /> <TodoList todos={this.state.todos.filter(FILTERS[this.state.filter])} toggleTodo={this.updateTodo}
 updateTodo={this.updateTodo} removeTodo={this.removeTodo} /> <Footer todos={this.state.todos} filters={FILTERS} activeFilter={this.state.filter} changeFilter={this.changeFilter} clearCompletedTodos={this.clearCompletedTodos} /> </div> ); } }
  35. export default class TodoList extends React.Component { static propTypes =

    { toggleTodo: React.PropTypes.func.isRequired, removeTodo: React.PropTypes.func.isRequired, updateTodo: React.PropTypes.func.isRequired, todos: React.PropTypes.array.isRequired, }; render() { return ( <ul className="list"> {this.props.todos.map((todo) => ( <TodoItem key={todo.id} todo={todo} toggleTodo={this.props.updateTodo} toggleTodo={this.props.updateTodo} removeTodo={this.props.removeTodo} /> ))} </ul> ); } }
  36. None
  37. None
  38. Dispatcher Action View Store

  39. Dispatcher Action API View Store

  40. class TodosStore extends ReduceStore { getInitialState() { return []; }

    reduce (state, action) { switch (action.type) { case 'todo/add': return [action.todo].concat(state); case 'todo/toggle': return state.map((t) => t.id === action.id ? { ...t, completed: case 'todo/remove': return state.filter((t) => t.id !== action.id); case 'todo/clear': return state.filter((t) => !t.completed) default: return state; } } }
  41. const dispatcher = new Dispatcher(); const store = new TodosStore(dispatcher);

    store.getState() // => []

  42. const dispatcher = new Dispatcher(); const store = new TodosStore(dispatcher);

    store.getState() // => []
 dispatcher.dispatch({ type: 'todo/add' text: 'Task 1' }); store.getState() // => [{ id: 1, text: 'Task 1', completed: false }]
  43. const dispatcher = new Dispatcher(); const store = new TodosStore(dispatcher);

    store.getState() // => []
 dispatcher.dispatch({ type: 'todo/add' text: 'Task 1' }); store.getState() // => [{ id: 1, text: 'Task 1', completed: false }] dispatcher.dispatch({ type: 'todo/toggle', id: 1 }); store.getState() // => [{ id: 1, text: 'Task 1', completed: true }]
  44. const dispatcher = new Dispatcher(); const store = new TodosStore(dispatcher);

    store.getState() // => []
 dispatcher.dispatch({ type: 'todo/add' text: 'Task 1' }); store.getState() // => [{ id: 1, text: 'Task 1', completed: false }] dispatcher.dispatch({ type: 'todo/toggle', id: 1 }); store.getState() // => [{ id: 1, text: 'Task 1', completed: true }] dispatcher.dispatch({ type: 'todo/remove', id: 1 }); store.getState() // => []
  45. class FilterStore extends ReduceStore { getInitialState() { return { name:

    'All', filter: Filters['All'] }; } reduce (state, action) { switch (action.type) { case 'filter/select': const name = action.filterName; const filter = FILTERS[name]; if (!filter) { return state; } return { name: name, filter: filter }; default: return state; } } }
  46. import {Container} from 'flux/utils'; class TodoList extends React.Component { static

    getStores() { return [TodosStore, FiltersStore]; } static calculateState() { return { todos: TodosStore.getState(), filter: FiltersStore.getState().filter, }; } render() { const todos = this.state.todos.filter(this.state.filter); return ( <ul className="list"> {todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)} </ul> ); } } export default Container.create(TodoList);
  47. import {Container} from 'flux/utils'; class TodoList extends React.Component { static

    getStores() { return [TodosStore, FiltersStore]; } static calculateState() { return { todos: TodosStore.getState(), filter: FiltersStore.getState().filter, }; } render() { const todos = this.state.todos.filter(this.state.filter); return ( <ul className="list"> {todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)} </ul> ); } } export default Container.create(TodoList);
  48. import {Container} from 'flux/utils'; class TodoList extends React.Component { static

    getStores() { return [TodosStore, FiltersStore]; } static calculateState() { return { todos: TodosStore.getState(), filter: FiltersStore.getState().filter, }; } render() { const todos = this.state.todos.filter(this.state.filter); return ( <ul className="list"> {todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)} </ul> ); } } export default Container.create(TodoList);
  49. import {Container} from 'flux/utils'; class TodoList extends React.Component { static

    getStores() { return [TodosStore, FiltersStore]; } static calculateState() { return { todos: TodosStore.getState(), filter: FiltersStore.getState().filter, }; } render() { const todos = this.state.todos.filter(this.state.filter); return ( <ul className="list"> {todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)} </ul> ); } } export default Container.create(TodoList);
  50. import {Container} from 'flux/utils'; class TodoList extends React.Component { static

    getStores() { return [TodosStore, FiltersStore]; } static calculateState() { return { todos: TodosStore.getState(), filter: FiltersStore.getState().filter, }; } render() { const todos = this.state.todos.filter(this.state.filter); return ( <ul className="list"> {todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)} </ul> ); } } export default Container.create(TodoList);
  51. import {Container} from 'flux/utils'; class TodoList extends React.Component { static

    getStores() { return [TodosStore, FiltersStore]; } static calculateState() { return { todos: TodosStore.getState(), filter: FiltersStore.getState().filter, }; } render() { const todos = this.state.todos.filter(this.state.filter); return ( <ul className="list"> {todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)} </ul> ); } } export default Container.create(TodoList);
  52. import {Container} from 'flux/utils'; class TodoList extends React.Component { static

    getStores() { return [TodosStore, FiltersStore]; } static calculateState() { return { todos: TodosStore.getState(), filter: FiltersStore.getState().filter, }; } render() { const todos = this.state.todos.filter(this.state.filter); return ( <ul className="list"> {todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)} </ul> ); } } export default Container.create(TodoList);
  53. export default class TodoItem extends React.Component { static propTypes =

    { todo: React.PropTypes.object.isRequired, }; handleRemove() { removeTodo(this.props.todo); } handleToggle() { toggleTodo(this.props.todo); } render() { const todo = this.props; return ( <li className={todo.completed ? 'completed' : ''}> <CheckBox checked={todo.completed} onClick={this.handleToggle.bind(thi <label>{todo.text}</label> <DeleteButton onClick={this.handleRemove.bind(this)} /> </li> ); } }
  54. export default class TodoItem extends React.Component { static propTypes =

    { todo: React.PropTypes.object.isRequired, }; handleRemove() { removeTodo(this.props.todo); } handleToggle() { toggleTodo(this.props.todo); } render() { const todo = this.props; return ( <li className={todo.completed ? 'completed' : ''}> <CheckBox checked={todo.completed} onClick={this.handleToggle.bind(thi <label>{todo.text}</label> <DeleteButton onClick={this.handleRemove.bind(this)} /> </li> ); } }
  55. import {dispatch} from 'stores/dispatcher'; 
 export function toggleTodo(todo) { dispatch({

    type: 'todo/toggle', id: todo.id, }); } export function removeTodo(todo) { dispatch({ type: 'todo/remove', id: todo.id, }); } export function clearCompletedTodos() { dispatch({ type: 'todo/clear', }); }
  56. import {dispatch} from 'stores/dispatcher'; 
 export function toggleTodo(todo) { dispatch({

    type: 'todo/toggle', id: todo.id, }); } export function removeTodo(todo) { dispatch({ type: 'todo/remove', id: todo.id, }); } export function clearCompletedTodos() { dispatch({ type: 'todo/clear', }); }
  57. const dispatcher = new Dispatcher(); const store = new TodosStore(dispatcher);

  58. • Global state • Communication between multiple stores • Dispatch

    locks • Testing Issues with FLUX
  59. None
  60. None
  61. Redux

  62. Dispatcher Action View Store

  63. Dispatcher Action View Store Store

  64. Action View Store Reducer(s) Reducer(s)

  65. let store = { todos: [], filter: 'All', };

  66. let store = { todos: [], filter: 'All', }; store

    = reduce(store, {action: 'todo/add', todo: todo}); store.todos // # => [ todo ]
  67. let store = { todos: [], filter: 'All', }; store

    = reduce(store, {action: 'todo/add', todo: todo}); store.todos // # => [ todo ] store = reduce(store, {action: 'todo/toggle', id: todo.id}); store = reduce(store, {action: 'todo/clear', id: todo.id}); store.todos // # => [ ]
  68. let store = { todos: [], filter: 'All', }; store

    = reduce(store, {action: 'todo/add', todo: todo}); store.todos // # => [ todo ] store = reduce(store, {action: 'todo/toggle', id: todo.id}); store = reduce(store, {action: 'todo/clear', id: todo.id}); store.todos // # => [ ] store = reduce(store, {action: 'filter/select', name: 'Active'}); store.filter // # => 'Active'
  69. Store State: todo
 
 todoReducer State: filter
 
 filterReducer

  70. // reducers/index.js
 import {combineReducers} from 'redux' import todos from './todos'

    import visibilityFilter from './visibilityFilter' export default combineReducers({ todos: todos, visibilityFilter: visibilityFilter, });
  71. class TodosStore extends ReduceStore { getInitialState() { return []; }

    reduce (state, action) { switch (action.type) { case 'todo/add': return [action.todo].concat(state); case 'todo/toggle': return state.map((t) => t.id === action.id ? { ...t, completed: case 'todo/remove': return state.filter((t) => t.id !== action.id); case 'todo/clear': return state.filter((t) => !t.completed) default: return state; } } }
  72. // reducers/todos.js export default function(state = [], action) { switch

    (action.type) { case 'todo/add': return [action.todo].concat(state); case 'todo/toggle': return state.map((t) => t.id === action.id ? { ...t, completed: t case 'todo/remove': return state.filter((t) => t.id !== action.id); case 'todo/clear': return state.filter((t) => !t.completed) default: return state; } }
  73. // reducers/visibilityFilter.js export default function(state, action) { if (!state) {

    state = DEFAULT_STATE; } switch (action.type) { case 'filter/select': const name = action.filterName; const filter = FILTERS[name]; if (!filter) { return state; } return { name: name, filter: filter }; default: return state; } }
  74. // reducers/index.js
 import {combineReducers} from 'redux' import todos from './todos'

    import visibilityFilter from './visibilityFilter' export default combineReducers({ todos: todos, visibilityFilter: visibilityFilter, });
  75. import React from 'react' import {render} from 'react-dom' import {Provider}

    from 'react-redux' import {createStore} from 'redux' import AppReducer from './reducers' import App from './components/App' let store = createStore(AppReducer) let root = ( <Provider store={store}> <App /> </Provider> ); render(root, document.getElementById('root'));
  76. <App> <NewTodo /> <TodoList> <TodoItem> </TodoList> <Footer> <Filters /> </Footer>

    </App> Store [todos] [filter]
  77. export default class App extends React.Component { render() { return

    ( <div className="app"> <NewTodo /> <TodoList /> <Footer /> </div> ); } }
  78. <App> <NewTodo /> <TodoList> <TodoItem> </TodoList> <Footer> <Filters /> </Footer>

    </App>
  79. import {connect} from 'react-redux' class TodoList extends React.Component { render()

    { const todos = this.props.todos.filter(this.props.filter); return ( <ul className="list"> {todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)} </ul> ); } } const decorate = connect((state) => { return { todos: state.todos, filter: state.visibilityFilter.filter, }; }); export default decorate(TodoList);
  80. import {connect} from 'react-redux' class TodoList extends React.Component { render()

    { const todos = this.props.todos.filter(this.props.filter); return ( <ul className="list"> {todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)} </ul> ); } } const decorate = connect((state) => { return { todos: state.todos, filter: state.visibilityFilter.filter, }; }); export default decorate(TodoList);
  81. import {connect} from 'react-redux' class TodoList extends React.Component { render()

    { const todos = this.props.todos.filter(this.props.filter); return ( <ul className="list"> {todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)} </ul> ); } } const decorate = connect((state) => { return { todos: state.todos, filter: state.visibilityFilter.filter, }; }); export default decorate(TodoList);
  82. import {connect} from 'react-redux' class TodoList extends React.Component { render()

    { const todos = this.props.todos.filter(this.props.filter); return ( <ul className="list"> {todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)} </ul> ); } } const decorate = connect((state) => { return { todos: state.todos, filter: state.visibilityFilter.filter, }; }); export default decorate(TodoList);
  83. import {connect} from 'react-redux' class TodoList extends React.Component { render()

    { const todos = this.props.todos.filter(this.props.filter); return ( <ul className="list"> {todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)} </ul> ); } } const decorate = connect((state) => { return { todos: state.todos, filter: state.visibilityFilter.filter, }; }); export default decorate(TodoList);
  84. import {connect} from 'react-redux' class TodoList extends React.Component { render()

    { const todos = this.props.todos.filter(this.props.filter); return ( <ul className="list"> {todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)} </ul> ); } } const decorate = connect((state) => { return { todos: state.todos, filter: state.visibilityFilter.filter, }; }); export default decorate(TodoList);
  85. import {connect} from 'react-redux' class TodoList extends React.Component { render()

    { const todos = this.props.todos.filter(this.props.filter); return ( <ul className="list"> {todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)} </ul> ); } } const decorate = connect((state) => { return { todos: state.todos, filter: state.visibilityFilter.filter, }; }); export default decorate(TodoList);
  86. Let's write some tests

  87. import {connect} from 'react-redux' export class TodoList extends React.Component {

    render() { const todos = this.props.todos.filter(this.props.filter); return ( <ul className="list"> {todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)} </ul> ); } } const decorate = connect((state) => { return { todos: state.todos, filter: state.visibilityFilter.filter, }; }); export default decorate(TodoList);
  88. import {connect} from 'react-redux' export class TodoList extends React.Component {

    render() { const todos = this.props.todos.filter(this.props.filter); return ( <ul className="list"> {todos.map((todo) => <TodoItem key={todo.id} todo={todo} />)} </ul> ); } } const decorate = connect((state) => { return { todos: state.todos, filter: state.visibilityFilter.filter, }; }); export default decorate(TodoList);
  89. // Using: chai, enzyme, chai-enzyme import {TodoList} from 'components/TodoList'; import

    {TodoItem} from 'components/TodoItem'; describe(TodoList.dispalyName, () => { const all = () => true const none = () => false it("renders list of TodoItem(s)", () => { const todos = [fakeTodo()];
 const element = shallow(<TodoList todos={todos} filter={all} />); 
 expect(element).to.have.descendants(<TodoItem />); }); it("filters with filter", () => { const todos = [fakeTodo()]; const element = shallow(<TodoList todos={todos} filter={none} />); expect(element).to.have.descendants(<TodoItem />); }); });
  90. <App> <NewTodo /> <TodoList> <TodoItem> </TodoList> <Footer> <Filters /> </Footer>

    </App>
  91. export class TodoItem extends React.Component { static propTypes = {

    todo: React.PropTypes.object.isRequired, dispatch: React.PropTypes.func.isRequired, }; handleRemove() { this.props.dispatch(removeTodo(this.props.todo)); } handleToggle() { this.props.dispatch(toggleTodo(this.props.todo)); } render() { return ( <li className={this.props.todo.completed ? 'completed' : ''}> <CheckBox checked={this.props.todo.completed} onClick={this.handleTogg <label>{this.props.todo.text}</label> <DeleteButton onClick={this.handleRemove.bind(this)} /> </li> ); } } export default connect()(TodoItem);
  92. 
 export function toggleTodo(todo) { return { type: 'todo/toggle', id:

    todo.id, }; } export function removeTodo(todo) { return { type: 'todo/remove', id: todo.id, }; } export function clearCompletedTodos() { return { type: 'todo/clear', }; }
  93. None
  94. None
  95. Redux friends • selectors / https://github.com/reactjs/reselect / • async /

    https://github.com/gaearon/redux-thunk / • router / https://github.com/reactjs/react-router-redux / • data fetching / https://github.com/relax/relate /
  96. https://speakerdeck.com/rstankov/react-and-redux

  97. https://github.com/rstankov/talks-code

  98. None
  99. None
  100. @rstankov Thanks :)

  101. Questions?