Flux / Redux

Лекция 28

APPLICATION ARCHITECTURE
FOR BUILDING USER INTERFACES

Flux

Flux

Flux

Flux

Flux

Flux - Facebook Issue

  • прислали сообщение
  • увеличить счетчик непрочитанных
  • добавить сообщение в боковой чат
  • если открыта страница сообщений, добавить сообщение в нее
  • если открыт собеседник или боковой чат в фокусе, уменьшить счетчик непрочитанных

Flux

Flux - Actions

  • сделать какой-то асинхронный запрос (опционально)
  • передать событие в Dispatcher
  • событие - специальная константа + данные

Flux - Actions


const FileConstants = {
  FILE_ADD: 'FILE_ADD',
  FILE_UPDATE_CONTENT: 'FILE_UPDATE_CONTENT',
  COMPILE_AUTO: 'COMPILE_AUTO'
};

export default FileConstants;
      

Flux - Actions


import Dispatcher from '../dispatcher';
import FileConstants from '../constants/file';
import Api from '../api';

const FileActions = {
  add: function (name) {
    Dispatcher.dispatch({
      actionType: FileConstants.FILE_ADD,
      name: name
    });
  },

  ...
};

export default FileActions;
      

Flux - Actions


const FileActions = {
  ...

  compile: function (files, resultId) {
    Api.compile(files).then((result) => {
      Dispatcher.dispatch({
        actionType: FileConstants.FILE_UPDATE_CONTENT,
        id: resultId,
        content: result
      });
    });
  }
};

export default FileActions;
      

Flux - Dispatcher

  • обеспечить передачу событий в Store
  • управлять зависимостями Store (опционально)

Flux - Dispatcher


const Dispatcher = {
  subscribers: [],

  subscribe: function (handler) {
    this.subscribers.push(handler);
  },

  dispatch: function (action) {
    for (let i = 0; i < this.subscribers.length; i++)
      this.subscribers[i](action);
  }
};

export default Dispatcher;
      

Flux - Store

  • состояние
  • бизнес-логика
  • обеспечить оповещение об изменении данных для View

Flux - Store


class Store {
  constructor() {
    this.seed = 0;
    this.data = [];
    this.subscribers = [];
  }

  getAll() {
    return this.data;
  }

  ...
}

export default Store;
      

Flux - Store


class Store {
  ...

  getOne(id) {
    return this.data.find(x => x.id);
  }

  add(item) {
    item.id = Date.now() + this.seed++;
    this.data.push(item);
  }

  ...
}
      

Flux - Store


class Store {
 ...

  update(item) {
    const index = this.data
      .findIndex(x => x.id === item.id);

    this.data[index] = Object.assign({}, this.data[index], item);
  }

  remove(id) {
    const index = this.data
      .findIndex(x => x.id === id);

    this.data.splice(index, 1);
  }
  ...
}
      

Flux - Store


class Store {
  ...
  emit() {
    for (let i = 0; i < this.subscribers.length; i++){
      this.subscribers[i]();
    }
  }

  subscribe(handler) {
    this.subscribers.push(handler);
  }

  unsubscribe(handler) {
    const index = this.subscribers.indexOf(handler);

    this.subscribers.splice(index, 1);
  }
}
      

Flux - Store


class FileStore extends Store {
  find(name) {
    return this.data.find(x => x.name === name);
  }
}

const fileStore = new FileStore();

...

export default fileStore;
      

Flux - Store


Dispatcher.subscribe((action) => {
  switch (action.actionType) {
    case FileConstants.FILE_ADD: {
      let file = fileStore.find(action.name);

      if (!file) return;

      file = { name: action.name, content: '' };
      fileStore.add(file);
      break;
    }

    ...
  }

  fileStore.emit();
});
      

Flux - Store


Dispatcher.subscribe((action) => {
  switch (action.actionType) {
    ...

    case FileConstants.FILE_UPDATE_CONTENT: {
        fileStore.update({
          id: action.id,
          content: action.content
        });

        break;
    }

    default: return;
  }

  fileStore.emit();
});
      

Flux - View

  • отобразить интерфейс с данными
  • подписаться на изменение данных
  • Controller-View

Flux - View


import React from 'react';

import FileActions from './actions/file';
import FileStore from './stores/file';

class App extends React.Component {
  render() {
    return (<div className="app-root">
      <Tabs tabs={this.state.tabs}/>
      <Textbox file={this.state.currentFile}
        compile={this._compile}/>
    </div>);
  }

  ...
});
      

Flux - View


class App extends React.Component {
  ...

  constructor(props) {
    super(props);

    this._onChange = ::this._onChange;
    this._compile = ::this._onChange;

    this.state = App.getState();
  }
  ...
});
      

Flux - View


class App extends React.Component {
  ...

  componentDidMount() {
    TabStore.subscribe(this._onChange);
    FileStore.subscribe(this._onChange);
  }

  componentWillUnmount() {
    TabStore.unsubscribe(this._onChange);
    FileStore.unsubscribe(this._onChange);
  }

  ...
});
      

Flux - View


class App extends React.Component {
  ...

  _onChange() {
    this.setState(
      App.getState()
    );
  }

  _compile() {
    FileActions.compile(
      this.state.files
    );
  }

  ...
});
      

Flux - View


class App extends React.Component {
  ...

  static getState() {
    return {
      tabs: TabStore.getAll(),
      files: FileStore.getAll(),
      currentFile: FileStore.getOne(
        TabStore.getCurrent().id
      )
    };
  }
});
      

Flux - Lifecycle

Flux - Lifecycle

Flux - Lifecycle

Flux - Lifecycle

Flux - Lifecycle

Flux - Lifecycle

Flux - Lifecycle

Flux - Lifecycle

Flux - Lifecycle

Todo (Flux)

Todo (Flux) - Constants


const TodoConstants = {
  TODO_INIT: 'TODO_INIT',
  TODO_ADD: 'TODO_ADD',
  TODO_REMOVE: 'TODO_REMOVE',
  TODO_UPDATE: 'TODO_UPDATE',
  TODO_TOGGLE: 'TODO_TOGGLE',
  TODO_TOGGLE_ALL: 'TODO_TOGGLE_ALL',
  TODO_CLEAR: 'TODO_CLEAR'
};

const NavConstants = {
  NAV_INIT: 'NAV_INIT',
  NAV_ACTIVATE: 'NAV_ACTIVATE'
};
      

Todo (Flux) - Actions


const TodoActions = {
  init: function () {
    TodoActions.add('Sleep');
    TodoActions.add('Eat');
    TodoActions.add('Code');
    TodoActions.add('Repeat');
  },

  add: function (text) {
    Dispatcher.dispatch({
      actionType: TodoConstants.TODO_ADD,
      text: text
    });
  },

  ...
};
      

Todo (Flux) - Actions


const TodoActions = {
  ...
  remove: function (id) {
    Dispatcher.dispatch({
      actionType: TodoConstants.TODO_REMOVE,
      id: id
    });
  },

  update: function (id, text) {
    Dispatcher.dispatch({
      actionType: TodoConstants.TODO_UPDATE,
      id: id,
      text: text
    });
  },
  ...
};
      

Todo (Flux) - Actions


const TodoActions = {
  ...
  toggle: function (id) {
    Dispatcher.dispatch({
      actionType: TodoConstants.TODO_TOGGLE,
      id: id
    });
  },

  toggleAll: function () {
    Dispatcher.dispatch({
      actionType: TodoConstants.TODO_TOGGLE_ALL
    });
  },
  ...
};
      

Todo (Flux) - Actions


const TodoActions = {
  ...
  clear: function () {
    Dispatcher.dispatch({
      actionType: TodoConstants.TODO_CLEAR
    });
  }
};
      

Todo (Flux) - Actions


const NavActions = {
  init: function () {
    Dispatcher.dispatch({
      actionType: NavConstants.NAV_INIT
    });
  },

  activate: function (link) {
    Dispatcher.dispatch({
      actionType: NavConstants.NAV_ACTIVATE,
      link: link
    });
  }
};
      

Todo (Flux) - Stores


class TodoStore {
  constructor() {...}
  getActiveCount() {...}
  getCompletedCount() {...}
  areAllCompleted() {...}
  addItem(text) {...}
  removeItem(id) {...}
  removeCompleted() {...}
  updateItem(id, text) {...}
  toggleItem(id) {...}
  switchAllTo(completed) {...}
  emit() {...}
  subscribe(handler) {...}
  unsubscribe(handler) {...}
  ...
}
      

Todo (Flux) - Stores


class TodoStore {
  ...

  getItems() {
    if (!this.activeLink
      || this.activeLink.title === 'All'
    ) {
      return this.list;
    } else if (
      this.activeLink.title === 'Completed'
    ) {
      return this.list.filter(x => x.completed);
    } else {
      return this.list.filter(x => !x.completed);
    }
  }
}
      

Todo (Flux) - Stores


const todoStore = new TodoStore();

Dispatcher.subscribe((action) => {
  const type = action.actionType;

  if (type === TodoConstants.TODO_ADD) {
    todoStore.addItem(action.text);
  } else if (type === TodoConstants.TODO_REMOVE) {
    todoStore.removeItem(action.id);
  } else if (type === TodoConstants.TODO_UPDATE) {
    todoStore.updateItem(action.id, action.text);
  } else if (type === TodoConstants.TODO_TOGGLE) {
    todoStore.toggleItem(action.id);
  } else if ...
});
      

Todo (Flux) - Stores


Dispatcher.subscribe((action) => {
  ... else if (type
      === TodoConstants.TODO_TOGGLE_ALL) {
    todoStore.toggleAll(
      !todoStore.areAllCompleted()
    );
  } else if (type === TodoConstants.TODO_CLEAR) {
    todoStore.removeCompleted();
  } else if (type === NavConstants.NAV_ACTIVATE) {
    todoStore.activeLink = action.link;
    return;
  } else return;

  todoStore.emit();
});
      

Todo (Flux) - Stores


class NavStore {
  constructor() {...}
  getLinks() {...}
  getActive() {...}
  setActive(link) {...}
  emit() {...}
  subscribe(handler) {...}
  unsubscribe(handler) {...}
}
      

Todo (Flux) - Stores


const navStore = new NavStore();

Dispatcher.subscribe((action) => {
  const type = action.actionType;

  if (type === NavConstants.NAV_INIT){
    navStore.setActive(navStore.links[0]);
  } else if (type === NavConstants.NAV_ACTIVATE) {
    navStore.setActive(action.link);
  } else return;

  navStore.emit();
});
      

Todo (Flux) - View


class ToDo extends React.Component {
  render() {...}

  ...
}
      

Todo (Flux) - View


constructor(props) {
  super(props);

  TodoActions.init();
  NavActions.init();

  this.state = ToDo.getState();
}
      

Todo (Flux) - View


componentDidMount() {
  todoStore.subscribe(this._rerender);
  navStore.subscribe(this._rerender);
}

componentWillUnmount() {
  todoStore.unsubscribe(this._rerender);
  navStore.unsubscribe(this._rerender);
}
      

Todo (Flux) - View


static getState() {
  return {
    remains: todoStore.getActiveCount(),
    completed: todoStore.getCompletedCount(),
    areAllCompleted: todoStore.areAllCompleted(),
    tasks: todoStore.getItems(),

    links: navStore.getLinks(),
    activeLink: navStore.getActive()
  };
}
      

Todo (Flux) - View


_rerender = () => {
  this.setState(ToDo.getState());
}

_toggleItem(id) {
  TodoActions.toggle(id);
}

_toogleAll() {
  TodoActions.toggleAll();
}

_removeItem(id) {
  TodoActions.remove(id);
}
      

Todo (Flux) - View


_addItem(text) {
  TodoActions.add(text);
}

_updateItem(id, text) {
  TodoActions.update(id, text);
}

_removeCompleted() {
  TodoActions.clear();
}

_navigate(link) {
  NavActions.activate(link);
}
      

Flux Utils

Flux Utils

  • набор утилит для Flux-архитектуры
  • Dispatcher
  • Store
  • Reduce Store
  • Container

Flux Utils - Install


npm install --save flux
      

<script src=
  "https://unpkg.com/flux@3.1.2/dist/Flux.js"
></script>

<script src=
  "https://unpkg.com/flux@3.1.2/dist/FluxUtils.js"
></script>
      

Flux Utils - Dispatcher


// or use Flux object if in sandbox
import { Dispatcher } from 'flux';

const dispatcher = new Dispatcher();
      

Flux Utils - Dispatcher


register(function callback): string

unregister(string id): void

waitFor(array<string> ids): void

dispatch(object payload): void

isDispatching(): boolean
      

Flux Utils - Dispatcher


const flightDispatcher = new Dispatcher();

const CountryStore = {country: null};
const CityStore = {city: null};

const capitals = {
  'france': 'paris',
  'usa': 'washington',
  'italy': 'rome'
};
...
      

Flux Utils - Dispatcher


...
CityStore.dispatchToken
    = flightDispatcher.register((payload) => {

  const type = payload.actionType;

  if (type === 'city-update') {
    CityStore.city = payload.selectedCity;
  } else if (type === 'country-update') {
    flightDispatcher
      .waitFor([CountryStore.dispatchToken]);

    CityStore.city
      = capitals[CountryStore.country];
  }

});
...
      

Flux Utils - Dispatcher


...
CountryStore.dispatchToken
    = flightDispatcher.register((payload) => {

  if (payload.actionType === 'country-update') {
    CountryStore.country
      = payload.selectedCountry;
  }

});
...
      

Flux Utils - Dispatcher


...
flightDispatcher.dispatch({
  actionType: 'country-update',
  selectedCountry: 'usa'
});

flightDispatcher.dispatch({
  actionType: 'city-update',
  selectedCity: 'new york'
});
      

Flux Utils - Store


// or use FluxUtils object if in sandbox
import { Store } from 'flux/utils';

class MyStore extends Store {...}
      

Flux Utils - Store


constructor(dispatcher: Dispatcher)

addListener(callback: Function)
  : {remove: Function}

getDispatcher(): Dispatcher

getDispatchToken(): DispatchToken

hasChanged(): boolean

__emitChange(): void

__onDispatch(payload: Object): void
      

Flux Utils - Store


const flightDispatcher = new Flux.Dispatcher();

const capitals = {
  'france': 'paris',
  'usa': 'washington',
  'italy': 'rome'
};
...
      

Flux Utils - Store


...
class CountryStore extends FluxUtils.Store {
  constructor(dispatcher) {
    super(dispatcher);
    this.country = null;
  }

  __onDispatch(payload) {
    if (payload.actionType === 'country-update') {
      this.country = payload.selectedCountry;
    }
  }

  getCountry() {
    return this.country;
  }
}
...
      

Flux Utils - Store


...
class CityStore extends FluxUtils.Store {
  constructor(dispatcher, countryStore, capitals) {
    super(dispatcher);

    this.city = null;
    this.countryStore = countryStore;
    this.capitals = capitals;
  }

  getCity() {
    return this.city;
  }
  ...
}
...
      

Flux Utils - Store


...
__onDispatch(payload) {
  const type = payload.actionType;

  if (type === 'city-update') {
    this.city = payload.selectedCity;
  } else if (type === 'country-update') {
    this.getDispatcher().waitFor([
      this.countryStore.getDispatchToken()
    ]);

    this.city = this.capitals[
      this.countryStore.getCountry()
    ];
  }
}
...
      

Flux Utils - Store


...
const countryStore = new CountryStore(
  flightDispatcher
);

const cityStore = new CityStore(
  flightDispatcher,
  countryStore,
  capitals
);
...
      

Flux Utils - Store


...
flightDispatcher.dispatch({
  actionType: 'country-update',
  selectedCountry: 'usa'
});

flightDispatcher.dispatch({
  actionType: 'city-update',
  selectedCity: 'new york'
});
      

Flux Utils - Reduce Store


// or use FluxUtils object if in sandbox
import { ReduceStore } from 'flux/utils';

class MyStore extends ReduceStore {...}
      

Flux Utils - Reduce Store


extends Store

getState(): T

getInitialState(): T

reduce(state: T, action: Object): T

areEqual(one: T, two: T): boolean
      

Flux Utils - Container


// or use FluxUtils object if in sandbox
import {Component} from 'react';
import {Container} from 'flux/utils';

class CounterContainer extends Component {
  ...
}

const container = Container.create(
  CounterContainer
);
      

Flux Utils - Container


class CounterContainer extends Component {
  static getStores() {
    return [CounterStore];
  }

  static calculateState(prevState) {
    return {
      counter: CounterStore.getState(),
    };
  }

  render() {
    return <CounterUI counter={this.state.counter} />;
  }
}
      

Redux

Redux - About

  • redux.js.org
  • организует хранение состояния
  • не только React
  • Flux + CQRS + Event Sourcing

Redux - Basics

  • один Store для хранения состояния приложения
  • reducer - чистая функция, которые на основании полученного состояния и события возвращают новое состояние
  • состояние не изменяется (read-only)
  • для больших приложений reducer разбивается на мелкие части

Redux - Install


npm install --save redux
      

<script src=
  "https://unpkg.com/redux@3.7.2/dist/redux.js"
></script>
      

Redux - Example


// or use 'Redux' object if in sandbox
import { createStore } from 'redux';

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

...
      

Redux - Example


...

let store = createStore(counter);

store.subscribe(() =>
  console.log(store.getState())
);

// 0
store.dispatch({ type: 'INCREMENT' });
// 1
store.dispatch({ type: 'INCREMENT' });
// 2
store.dispatch({ type: 'DECREMENT' });
// 1
      

Redux - Complex Reducer


function humans(state = [], action) {
  switch (action.type) {
    case 'HUMANS_ADD':
      return [
        ...state,
        { name: action.name }
      ];
  }

  return state;
}
...
      

Redux - Complex Reducer


...
function robots(state = [], action) {
  switch (action.type) {
    case 'ROBOTS_ADD':
      return [
        ...state,
        {
          name: action.name,
          manufacturer: action.manufacturer
        }
      ];
  }

  return state;
}
...
      

Redux - Complex Reducer


...
const reducer = Redux.combineReducers({
  humans,
  robots
});

const store = Redux.createStore(reducer);

store.subscribe(() =>
  console.log(store.getState())
);
...
      

Redux - Complex Reducer


...
store.dispatch({
  type: 'HUMANS_ADD',
  name: 'Fry' });

store.dispatch({
  type: 'HUMANS_ADD',
  name: 'Leela' });

store.dispatch({
  type: 'ROBOTS_ADD',
  name: 'Bender',
  manufacturer: 'MomCorp' });
      

Redux - Initial State


const store = Redux.createStore(reducer, {
  humans: [{ name: 'Pr. Farnsworth' }]
});
      

Todo (Redux)

Todo (Redux) - Actions


const TodoConstants = {
  TODO_ADD: 'TODO_ADD',
  TODO_REMOVE: 'TODO_REMOVE',
  TODO_UPDATE: 'TODO_UPDATE',
  TODO_TOGGLE: 'TODO_TOGGLE',
  TODO_TOGGLE_ALL: 'TODO_TOGGLE_ALL',
  TODO_CLEAR: 'TODO_CLEAR'
};

const NavConstants = {
  NAV_ACTIVATE: 'NAV_ACTIVATE'
};
      

Todo (Redux) - Actions


const TodoActions = {
  add: function (text) {
    return { type: TodoConstants.TODO_ADD, text };
  },

  remove: function (id) {
    return { type: TodoConstants.TODO_REMOVE, id };
  },

  update: function (id, text) {
    return { type: TodoConstants.TODO_UPDATE,
      id, text};
  },
  ...
};
      

Todo (Redux) - Actions


const TodoActions = {
  ...
  toggle: function (id) {
    return { type: TodoConstants.TODO_TOGGLE, id };
  },

  toggleAll: function (completed) {
    return { type: TodoConstants.TODO_TOGGLE_ALL,
      completed };
  },

  clear: function () {
    return { type: TodoConstants.TODO_CLEAR };
  }
};
      

Todo (Redux) - Actions


const NavActions = {
  activate: function (link) {
    return {
      type: NavConstants.NAV_ACTIVATE,
      link: link
    };
  }
};
      

Todo (Redux) - Reducers


function tasks(state = [], action) {
  switch (action.type) {
    ...

    default:
      return state;
  }
}
      

Todo (Redux) - Reducers


case TodoConstants.TODO_ADD:
  return [
    ...state,
    {
      id: Date.now() + state.length,
      text: action.text,
      completed: false
    }
  ];
      

Todo (Redux) - Reducers


case TodoConstants.TODO_REMOVE:
  return state.filter(
    item => item.id !== action.id
  );
      

Todo (Redux) - Reducers


case TodoConstants.TODO_UPDATE:
  return state.map(item => {
    if (item.id !== action.id) return item;

    return {
      ...item,
      text: action.text
    };
  });
      

Todo (Redux) - Reducers


case TodoConstants.TODO_TOGGLE:
  return state.map(item => {
    if (item.id !== action.id) return item;

    return {
      ...item,
      completed: !item.completed
    };
  });
      

Todo (Redux) - Reducers


case TodoConstants.TODO_TOGGLE_ALL:
  return state.map(item => {
    return {
      ...item,
      completed: action.completed
    };
  });
      

Todo (Redux) - Reducers


case TodoConstants.TODO_CLEAR:
  return state.filter(item => !item.completed);
      

Todo (Redux) - Reducers


function activeLink(state = {}, action) {
  switch (action.type) {
    case NavConstants.NAV_ACTIVATE:
      return action.link;

    default:
      return state;
  }
}
      

Todo (Redux) - Reducers


function links(state = []) {
  return state;
}
      

Todo (Redux) - Reducers


const reducer = Redux.combineReducers({
  tasks,
  activeLink,
  links
});
      

Todo (Redux) - Store


const store = Redux.createStore(reducer, {
  tasks: [
    { id: 1, text: 'Sleep', completed: true },
    { id: 2, text: 'Eat', completed: false },
    { id: 3, text: 'Code', completed: false },
    { id: 4, text: 'Repeat', completed: false }
  ],

  links: [
    { title: 'All' },
    { title: 'Active' },
    { title: 'Completed' }
  ],

  activeLink: { title: 'All' }
});
      

Todo (Redux) - View


class ToDo extends React.Component {
  constructor(props) {
    super(props);

    this.state = ToDo.getState();
    this.state.subscription = store.subscribe(
      this._rerender
    );
  }

  render: function () {...}

  ...
}
      

Todo (Redux) - View


componentWillUnmount() {
  this.state.subscription.unsubscribe();
}
      

Todo (Redux) - View


static getState() {
  const state = { ...store.getState() };

  state.completed = state.tasks.filter(x => x.completed).length;
  state.remains = state.tasks.length - state.completed;
  state.areAllCompleted = state.remains === 0;

  if (state.activeLink.title === 'Completed') {
    state.tasks = state.tasks.filter(x => x.completed);
  } else if (state.activeLink.title === 'Active') {
    state.tasks = state.tasks.filter(x => !x.completed);
  }

  return state;
}
      

Todo (Redux) - View


_rerender = () => {
  this.setState(ToDo.getState());
}

_toggleItem(id) {
  store.dispatch(
    TodoActions.toggle(id)
  );
}

_toogleAll = () => {
  store.dispatch(
    TodoActions.toggleAll(!this.state.areAllCompleted)
  );
}
      

Todo (Redux) - View


_removeItem(id) {
  store.dispatch(
    TodoActions.remove(id)
  );
}

_addItem(text) {
  store.dispatch(
    TodoActions.add(text)
  );
}

_updateItem(id, text) {
  store.dispatch(
    TodoActions.update(id, text)
  );
}
      

Todo (Redux) - View


_removeCompleted() {
  store.dispatch(
    TodoActions.clear()
  );
}

_navigate(link) {
  store.dispatch(
    NavActions.activate(link)
  );
}
      

React Redux

React Redux - Install


npm install --save react-redux
      

<script src=
  "https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.7/react-redux.js"
></script>
      

React Redux - ToDo


function getVisibleTasks(tasks, link) {
  if (link.title === 'All') {

    return tasks;

  } else if (link.title === 'Completed') {

    return tasks.filter(x => x.completed);

  } else {

    return tasks.filter(x => !x.completed);

  }
}
      

React Redux - ToDo


function getCompletedCount(tasks) {
  return tasks.filter(x => x.completed).length;
}
      

React Redux - ToDo


function getRemainsCount(tasks) {
  return tasks.filter(x => !x.completed).length;
}
      

React Redux - ToDo


function areAllCompleted(tasks) {
  return getRemainsCount(tasks) === 0;
}
      

React Redux - ToDo


const mapStateToProps = (state) => {
  return {
    tasks: getVisibleTasks(
      state.tasks,
      state.activeLink
    ),

    completed: getCompletedCount(state.tasks),

    remains: getRemainsCount(state.tasks),

    areAllCompleted: areAllCompleted(state.tasks),

    activeLink: state.activeLink,

    links: state.links
  }
};
      

React Redux - ToDo


const mapDispatchToProps = (dispatch) => {
  return {
    toggleItem: (id) => {
      dispatch(
        TodoActions.toggle(id)
      );
    },

    toggleAll: (status) => {
      dispatch(
        TodoActions.toggleAll(status)
      );
    },
    ...
  };
};
      

React Redux - ToDo


const mapDispatchToProps = (dispatch) => {
  return {
    ...
    removeItem: (id) => {
      dispatch(
        TodoActions.remove(id)
      );
    },

    addItem: (text) => {
      dispatch(
        TodoActions.add(text)
      );
    },
    ...
  };
};
      

React Redux - ToDo


const mapDispatchToProps = (dispatch) => {
  return {
    ...
    updateItem: (id, text) => {
      dispatch(
        TodoActions.update(id, text)
      );
    },

    removeCompleted: () => {
      dispatch(
        TodoActions.clear()
      );
    },
    ...
  };
};
      

React Redux - ToDo


const mapDispatchToProps = (dispatch) => {
  return {
    ...
    navigate: (link) => {
      dispatch(
        NavActions.activate(link)
      );
    }
  };
};
      

React Redux - ToDo


class ToDo extends React.Component {
  render() {
    return (...);
  },

  _toggleAll = () => {
    this.props.toggleAll(
      !this.props.areAllCompleted
    );
  }
});
      

React Redux - ToDo


<div className="todo">
  <div className="todo__title">
    React Redux ToDo
  </div>

  <Nav
    links={this.props.links}
    activeLink={this.props.activeLink}
    navigate={this.props.navigate}
  />

  ...
</div>
      

React Redux - ToDo


<div className="todo">
  ...

  <ToDoSummary
    remains={this.props.remains}
    completed={this.props.completed}
  />

  ...
</div>
      

React Redux - ToDo


<div className="todo">
  ...

  <ToDoList
    tasks={this.props.tasks}
    areAllComplete={this.props.areAllCompleted}
    toggleItem={this.props.toggleItem}
    toggleAll={this._toggleAll}
    removeItem={this.props.removeItem}
    updateItem={this.props.updateItem}
  />

  ...
</div>
      

React Redux - ToDo


<div className="todo">
  ...

  <ToDoForm
    addItem={this.props.addItem}
  />

  <ToDoClear
    removeCompleted={this.props.removeCompleted}
  />
</div>
      

React Redux - ToDo


const ToDoContainer = ReactRedux.connect(
  mapStateToProps,
  mapDispatchToProps
)(ToDo);
      

React Redux - ToDo


ReactDOM.render(
  <ReactRedux.Provider store={store}>
    <ToDoContainer/>
  </ReactRedux.Provider>,
  document.getElementById('app')
);
      

React Redux - Async Action Creator


export function fetchPosts(subreddit) {
  return function (dispatch) {
    dispatch(requestPosts(subreddit));

    return fetch(`https://www.reddit.com/r/${subreddit}.json`)
      .then(
        response => response.json(),
        error => console.log('An error occurred.', error)
      )
      .then(json =>
        dispatch(receivePosts(subreddit, json))
      )
  }
}
      

React Redux - Middleware


// logging, reports, side-effects, ...
const logger = store => next => action => {
  console.log('dispatching', action)

  let result = next(action)

  console.log('next state', store.getState())

  return result
}

...
      

React Redux - Middleware


const crashReporter = store => next => action => {
  try {
    return next(action)
  } catch (err) {
    console.error('Caught an exception!', err)

    // TODO: report error

    throw err
  }
}
      

React Redux - Middleware


import {
  createStore,
  combineReducers,
  applyMiddleware
} from 'redux'

const todoApp = combineReducers(reducers)

const store = createStore(
  todoApp,
  applyMiddleware(logger, crashReporter)
)
      

Time Travel

Time Travel


// simple state
{
  counter: 10
}
      

Time Travel


// state for undo/redo feature
{
  counter: {
    past: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ],
    present: 10,
    future: []
  }
}
      

Time Travel


// UNDO X 1
{
  counter: {
    past: [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ],
    present: 9,
    future: [ 10 ]
  }
}
      

Time Travel


// UNDO X 2
{
  counter: {
    past: [ 0, 1, 2, 3, 4, 5, 6, 7 ],
    present: 8,
    future: [ 9, 10 ]
  }
}
      

Time Travel


{
  past: Array<T>,
  present: T,
  future: Array<T>
}
      

Time Travel


// Global Undo/Redo
{
  past: [
    { counterA: 1, counterB: 1 },
    { counterA: 1, counterB: 0 },
    { counterA: 0, counterB: 0 }
  ],
  present: { counterA: 2, counterB: 1 },
  future: []
}
      

Time Travel


// Separate Undo/Redo
{
  counterA: {
    past: [ 1, 0 ],
    present: 2,
    future: []
  },
  counterB: {
    past: [ 0 ],
    present: 1,
    future: []
  }
}
      

Time Travel - Undo Rules

  • извлечь последний элемент из past
  • добавить present в начало future
  • заменить present на извлеченный из past элемент

Time Travel - Redo Rules

  • извлечь первый элемент из future
  • добавить present в конец past
  • заменить present на извлеченный из future элемент

Time Travel - Other Action Rules

  • добавить present в конец past
  • установить present в обновленный state
  • очистить future

Time Travel - Install


npm install --save redux-undo
      

<script src=
  "https://unpkg.com/redux-undo@1.0.0-beta7/dist/redux-undo.js"
></script>
      

Time Travel - Use


import undoable from 'redux-undo';

undoable(reducer)
undoable(reducer, config)
      

Time Travel - Use


import { ActionCreators } from 'redux-undo';

store.dispatch(ActionCreators.undo());

ActionCreators.undo()
ActionCreators.redo()

ActionCreators.jump(-2) // undo 2 steps
ActionCreators.jump(5)  // redo 5 steps

ActionCreators.jumpToPast(index)
ActionCreators.jumpToFuture(index)

ActionCreators.clearHistory()
      

Time Travel - Use


undoable(reducer, {
  limit: false,
  filter: () => true,
  debug: false,
  neverSkipReducer: false,

  undoType: ActionTypes.UNDO,
  redoType: ActionTypes.REDO,

  jumpType: ActionTypes.JUMP,

  jumpToPastType: ActionTypes.JUMP_TO_PAST,
  jumpToFutureType: ActionTypes.JUMP_TO_FUTURE,

  clearHistoryType: ActionTypes.CLEAR_HISTORY
})
      

Time Travel - ToDo


const reducer = Redux.combineReducers({
  tasks: ReduxUndo.default(tasks),
  activeLink,
  links
});
      

Time Travel - ToDo


const store = Redux.createStore(reducer, {
  tasks: {
    past: [],
    present: [...],
    future: []
  },
  ...
});
      

Time Travel - ToDo


class TimeTravelPanel extends React.Component {
  render() {
    return (<div className="time-panel">
        <div className={'time-panel__button '
            + (this.props.canUndo ? ''
              : 'time-panel__button_disabled')}
          onClick={this.props.undo}>Undo</div>

        <div className={'time-panel__button '
          + (this.props.canRedo ? ''
            : 'time-panel__button_disabled')}
          onClick={this.props.redo}>Redo</div>
    </div>);
  }
}
      

Time Travel - ToDo


class ToDo extends React.Component {
  render() {
    return (
      <div className="todo">
        ...
        <TimeTravelPanel
          undo={this.props.undo}
          redo={this.props.redo}
          canUndo={this.props.canUndo}
          canRedo={this.props.canRedo}
        />
      </div>
    );
  }
  ...
});
      

Time Travel - ToDo


function canUndo(model) {
  return model.past
    && model.past.length !== 0;
}

function canRedo(model) {
  return model.future
    && model.future.length !== 0;
}
      

Time Travel - ToDo


const mapStateToProps = (state) => {
  return {
    tasks: getVisibleTasks(
      state.tasks.present,
      state.activeLink
    ),

    completed: getCompletedCount(
      state.tasks.present
    ),

    remains: getRemainsCount(
      state.tasks.present
    ),
    ...
  };
};
      

Time Travel - ToDo


const mapStateToProps = (state) => {
  return {
    ...
    areAllCompleted: areAllCompleted(
      state.tasks.present
    ),

    activeLink: state.activeLink,
    links: state.links,

    canUndo: canUndo(state.tasks),
    canRedo: canRedo(state.tasks)
  };
};
      

Time Travel - ToDo


const mapDispatchToProps = (dispatch) => {
  return {
    ...
    undo: () => {
      dispatch(
        ReduxUndo.ActionCreators.undo()
      );
    },

    redo: () => {
      dispatch(
        ReduxUndo.ActionCreators.redo()
      );
    }
  };
};