Slide 1

Slide 1 text

React & Redux Radoslav Stankov 27/03/2016

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

React

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

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 (
    {this.filteredTodos().map((todo) => (

Slide 12

Slide 12 text

render() { return (
    {this.filteredTodos().map((todo) => (
  • {todo.text} ))}
{this.counterText()}
{Object.keys(FILTERS).map((filterName) => (
); }

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

Slide 20

Slide 20 text

Slide 21

Slide 21 text

Slide 22

Slide 22 text

Slide 23

Slide 23 text

Slide 24

Slide 24 text

Slide 25

Slide 25 text

Slide 26

Slide 26 text

Slide 27

Slide 27 text

Where is all the data?

Slide 28

Slide 28 text

[todos] [filter]

Slide 29

Slide 29 text

export default class App extends React.Component { // . . . render() { return (
); } }

Slide 30

Slide 30 text

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 (
    {this.props.todos.map((todo) => ( ))}
); } }

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

Let’s add new action

Slide 33

Slide 33 text

[todos] [filter]

Slide 34

Slide 34 text

export default class App extends React.Component { // . . . render() { return (
); } }

Slide 35

Slide 35 text

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 (
    {this.props.todos.map((todo) => ( ))}
); } }

Slide 36

Slide 36 text

No content

Slide 37

Slide 37 text

No content

Slide 38

Slide 38 text

Dispatcher Action View Store

Slide 39

Slide 39 text

Dispatcher Action API View Store

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

const dispatcher = new Dispatcher(); const store = new TodosStore(dispatcher); store.getState() // => []


Slide 42

Slide 42 text

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 }]

Slide 43

Slide 43 text

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 }]

Slide 44

Slide 44 text

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() // => []

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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 (
    {todos.map((todo) => )}
); } } export default Container.create(TodoList);

Slide 47

Slide 47 text

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 (
    {todos.map((todo) => )}
); } } export default Container.create(TodoList);

Slide 48

Slide 48 text

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 (
    {todos.map((todo) => )}
); } } export default Container.create(TodoList);

Slide 49

Slide 49 text

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 (
    {todos.map((todo) => )}
); } } export default Container.create(TodoList);

Slide 50

Slide 50 text

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 (
    {todos.map((todo) => )}
); } } export default Container.create(TodoList);

Slide 51

Slide 51 text

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 (
    {todos.map((todo) => )}
); } } export default Container.create(TodoList);

Slide 52

Slide 52 text

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 (
    {todos.map((todo) => )}
); } } export default Container.create(TodoList);

Slide 53

Slide 53 text

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 (
  • {todo.text}
  • ); } }

    Slide 54

    Slide 54 text

    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 (
  • {todo.text}
  • ); } }

    Slide 55

    Slide 55 text

    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', }); }

    Slide 56

    Slide 56 text

    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', }); }

    Slide 57

    Slide 57 text

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

    Slide 58

    Slide 58 text

    • Global state • Communication between multiple stores • Dispatch locks • Testing Issues with FLUX

    Slide 59

    Slide 59 text

    No content

    Slide 60

    Slide 60 text

    No content

    Slide 61

    Slide 61 text

    Redux

    Slide 62

    Slide 62 text

    Dispatcher Action View Store

    Slide 63

    Slide 63 text

    Dispatcher Action View Store Store

    Slide 64

    Slide 64 text

    Action View Store Reducer(s) Reducer(s)

    Slide 65

    Slide 65 text

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

    Slide 66

    Slide 66 text

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

    Slide 67

    Slide 67 text

    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 // # => [ ]

    Slide 68

    Slide 68 text

    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'

    Slide 69

    Slide 69 text

    Store State: todo
 
 todoReducer State: filter
 
 filterReducer

    Slide 70

    Slide 70 text

    // reducers/index.js
 import {combineReducers} from 'redux' import todos from './todos' import visibilityFilter from './visibilityFilter' export default combineReducers({ todos: todos, visibilityFilter: visibilityFilter, });

    Slide 71

    Slide 71 text

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

    Slide 72

    Slide 72 text

    // 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; } }

    Slide 73

    Slide 73 text

    // 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; } }

    Slide 74

    Slide 74 text

    // reducers/index.js
 import {combineReducers} from 'redux' import todos from './todos' import visibilityFilter from './visibilityFilter' export default combineReducers({ todos: todos, visibilityFilter: visibilityFilter, });

    Slide 75

    Slide 75 text

    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 = ( ); render(root, document.getElementById('root'));

    Slide 76

    Slide 76 text

    Store [todos] [filter]

    Slide 77

    Slide 77 text

    export default class App extends React.Component { render() { return (
    ); } }

    Slide 78

    Slide 78 text

    Slide 79

    Slide 79 text

    import {connect} from 'react-redux' class TodoList extends React.Component { render() { const todos = this.props.todos.filter(this.props.filter); return (
      {todos.map((todo) => )}
    ); } } const decorate = connect((state) => { return { todos: state.todos, filter: state.visibilityFilter.filter, }; }); export default decorate(TodoList);

    Slide 80

    Slide 80 text

    import {connect} from 'react-redux' class TodoList extends React.Component { render() { const todos = this.props.todos.filter(this.props.filter); return (
      {todos.map((todo) => )}
    ); } } const decorate = connect((state) => { return { todos: state.todos, filter: state.visibilityFilter.filter, }; }); export default decorate(TodoList);

    Slide 81

    Slide 81 text

    import {connect} from 'react-redux' class TodoList extends React.Component { render() { const todos = this.props.todos.filter(this.props.filter); return (
      {todos.map((todo) => )}
    ); } } const decorate = connect((state) => { return { todos: state.todos, filter: state.visibilityFilter.filter, }; }); export default decorate(TodoList);

    Slide 82

    Slide 82 text

    import {connect} from 'react-redux' class TodoList extends React.Component { render() { const todos = this.props.todos.filter(this.props.filter); return (
      {todos.map((todo) => )}
    ); } } const decorate = connect((state) => { return { todos: state.todos, filter: state.visibilityFilter.filter, }; }); export default decorate(TodoList);

    Slide 83

    Slide 83 text

    import {connect} from 'react-redux' class TodoList extends React.Component { render() { const todos = this.props.todos.filter(this.props.filter); return (
      {todos.map((todo) => )}
    ); } } const decorate = connect((state) => { return { todos: state.todos, filter: state.visibilityFilter.filter, }; }); export default decorate(TodoList);

    Slide 84

    Slide 84 text

    import {connect} from 'react-redux' class TodoList extends React.Component { render() { const todos = this.props.todos.filter(this.props.filter); return (
      {todos.map((todo) => )}
    ); } } const decorate = connect((state) => { return { todos: state.todos, filter: state.visibilityFilter.filter, }; }); export default decorate(TodoList);

    Slide 85

    Slide 85 text

    import {connect} from 'react-redux' class TodoList extends React.Component { render() { const todos = this.props.todos.filter(this.props.filter); return (
      {todos.map((todo) => )}
    ); } } const decorate = connect((state) => { return { todos: state.todos, filter: state.visibilityFilter.filter, }; }); export default decorate(TodoList);

    Slide 86

    Slide 86 text

    Let's write some tests

    Slide 87

    Slide 87 text

    import {connect} from 'react-redux' export class TodoList extends React.Component { render() { const todos = this.props.todos.filter(this.props.filter); return (
      {todos.map((todo) => )}
    ); } } const decorate = connect((state) => { return { todos: state.todos, filter: state.visibilityFilter.filter, }; }); export default decorate(TodoList);

    Slide 88

    Slide 88 text

    import {connect} from 'react-redux' export class TodoList extends React.Component { render() { const todos = this.props.todos.filter(this.props.filter); return (
      {todos.map((todo) => )}
    ); } } const decorate = connect((state) => { return { todos: state.todos, filter: state.visibilityFilter.filter, }; }); export default decorate(TodoList);

    Slide 89

    Slide 89 text

    // 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(); 
 expect(element).to.have.descendants(); }); it("filters with filter", () => { const todos = [fakeTodo()]; const element = shallow(); expect(element).to.have.descendants(); }); });

    Slide 90

    Slide 90 text

    Slide 91

    Slide 91 text

    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 (
  • {this.props.todo.text}
  • ); } } export default connect()(TodoItem);

    Slide 92

    Slide 92 text

    
 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', }; }

    Slide 93

    Slide 93 text

    No content

    Slide 94

    Slide 94 text

    No content

    Slide 95

    Slide 95 text

    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 /

    Slide 96

    Slide 96 text

    https://speakerdeck.com/rstankov/react-and-redux

    Slide 97

    Slide 97 text

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

    Slide 98

    Slide 98 text

    No content

    Slide 99

    Slide 99 text

    No content

    Slide 100

    Slide 100 text

    @rstankov Thanks :)

    Slide 101

    Slide 101 text

    Questions?