Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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)

Radoslav Stankov

March 27, 2016
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

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

    View Slide

  2. Radoslav Stankov
    @rstankov

    http://rstankov.com

    http://github.com/rstankov

    View Slide

  3. View Slide

  4. View Slide

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

    View Slide

  6. View Slide

  7. View Slide

  8. View Slide

  9. React

    View Slide

  10. View Slide

  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 (




    {this.filteredTodos().map((todo) => (

    View Slide

  12. render() {
    return (




    {this.filteredTodos().map((todo) => (

    {todo.text}

    ))}


    {this.counterText()}
    {Object.keys(FILTERS).map((filterName) => (
    ))}


    );
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  17. View Slide

  18. View Slide










  19. View Slide










  20. View Slide










  21. View Slide










  22. View Slide










  23. View Slide










  24. View Slide










  25. View Slide










  26. View Slide

  27. Where is all the data?

    View Slide

  28. [todos] [filter]








    View Slide

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

    addTodo={this.addTodo} />
    todos={this.state.todos.filter(FILTERS[this.state.filter])}
    toggleTodo={this.updateTodo}
    removeTodo={this.removeTodo} />
    todos={this.state.todos}
    filters={FILTERS}
    activeFilter={this.state.filter}
    changeFilter={this.changeFilter}
    clearCompletedTodos={this.clearCompletedTodos} />

    );
    }
    }

    View Slide

  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 (

    {this.props.todos.map((todo) => (
    key={todo.id}
    todo={todo}
    toggleTodo={this.props.updateTodo}
    removeTodo={this.props.removeTodo} />
    ))}

    );
    }
    }

    View Slide

  31. View Slide

  32. Let’s add new action

    View Slide

  33. [todos] [filter]








    View Slide

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

    addTodo={this.addTodo} />
    todos={this.state.todos.filter(FILTERS[this.state.filter])}
    toggleTodo={this.updateTodo}

    updateTodo={this.updateTodo}
    removeTodo={this.removeTodo} />
    todos={this.state.todos}
    filters={FILTERS}
    activeFilter={this.state.filter}
    changeFilter={this.changeFilter}
    clearCompletedTodos={this.clearCompletedTodos} />

    );
    }
    }

    View Slide

  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 (

    {this.props.todos.map((todo) => (
    key={todo.id}
    todo={todo}
    toggleTodo={this.props.updateTodo}
    toggleTodo={this.props.updateTodo}
    removeTodo={this.props.removeTodo} />
    ))}

    );
    }
    }

    View Slide

  36. View Slide

  37. View Slide

  38. Dispatcher
    Action
    View
    Store

    View Slide

  39. Dispatcher
    Action API
    View
    Store

    View Slide

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

    View Slide

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


    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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 (

    {todos.map((todo) => )}

    );
    }
    }
    export default Container.create(TodoList);

    View Slide

  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 (

    {todos.map((todo) => )}

    );
    }
    }
    export default Container.create(TodoList);

    View Slide

  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 (

    {todos.map((todo) => )}

    );
    }
    }
    export default Container.create(TodoList);

    View Slide

  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 (

    {todos.map((todo) => )}

    );
    }
    }
    export default Container.create(TodoList);

    View Slide

  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 (

    {todos.map((todo) => )}

    );
    }
    }
    export default Container.create(TodoList);

    View Slide

  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 (

    {todos.map((todo) => )}

    );
    }
    }
    export default Container.create(TodoList);

    View Slide

  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 (

    {todos.map((todo) => )}

    );
    }
    }
    export default Container.create(TodoList);

    View Slide

  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 (

    {todo.text}


    );
    }
    }

    View Slide

  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 (

    {todo.text}


    );
    }
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  59. View Slide

  60. View Slide

  61. Redux

    View Slide

  62. Dispatcher
    Action
    View
    Store

    View Slide

  63. Dispatcher
    Action
    View
    Store Store

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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'

    View Slide

  69. Store
    State: todo


    todoReducer
    State: filter


    filterReducer

    View Slide

  70. // reducers/index.js

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  74. // reducers/index.js

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

    View Slide

  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 = (



    );
    render(root, document.getElementById('root'));

    View Slide










  76. Store
    [todos] [filter]

    View Slide

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





    );
    }
    }

    View Slide










  78. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  86. Let's write some tests

    View Slide

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

    View Slide

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

    View Slide

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

    expect(element).to.have.descendants();
    });
    it("filters with filter", () => {
    const todos = [fakeTodo()];
    const element = shallow();
    expect(element).to.have.descendants();
    });
    });

    View Slide










  90. View Slide

  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 (

    {this.props.todo.text}


    );
    }
    }
    export default connect()(TodoItem);

    View Slide


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

    View Slide

  93. View Slide

  94. View Slide

  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 /

    View Slide

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

    View Slide

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

    View Slide

  98. View Slide

  99. View Slide

  100. @rstankov
    Thanks :)

    View Slide

  101. Questions?

    View Slide