Иван Соснин, СКБ Контур

UralJS, февраль 2017

Управление армией

сайд-эффектов

bit.ly/uraljs_se

function updateCounter() {
    var itemsCount = $('.js-item').length;
    $('.js-count').text(itemsCount + ' item(s)');
}

$('.js-add-form').on('submit', function() {
    updateCounter();
});

$('.js-item').on('change', function() {
    updateCounter();
});

$('.js-delete').on('click', function() {
    updateCounter();
});

React

Redux

Redux

// reducer.js
const reducer = (oldState, action) => {
    switch (action.type) {
        case 'INCREMENT':
            return {count: oldState.count + 1};
    }
};
// View.jsx
dispatch({type: 'INCREMENT'});
// View.jsx
connect();

Масштабирование

Тестирование

dispatch('Increment', () => {
    it('should increment', () => {
        expect(reducer({count: 1}, {type: 'INCREMENT'}))
            .to.deep.equal({count: 2});
    });
});

И все стало хорошо!

Или нет?

Async Action

// asyncActions.js
export const fetchData = (dispatch, isAdmin) => {
    dispatch(actions.fetchDataBegin());

    axios
        .get('/api/someData', params: { isAdmin: isAdmin })
        .then({ data } => dispatch(actions.fetchDataSuccess(data)))
        .catch(e => dispatch(actions.fetchDataError(e)));
};

// ButtonComponent.jsx
fetchData(this.props.dispatch, this.props.isAdmin);

Async Action

Легко использовать

Сложно тестировать

Нет всего стора

// loggerMiddleware.js
const logger = store => dispatch => action => {
    console.log('dispatching', action);
    console.log('next state', store.getState());

    return dispatch(action);
}

Redux Thunk

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

Redux Thunk

Сайд-эффекты в middleware

Контекст приложения, а не компонента

Сложно тестировать

function* dramaticTurnOfEvents() {
    yield "Luke";
    yield "I am";
    yield "your";

    return "father";
}
const iterator = dramaticTurnOfEvents();
iterator.next(); // { value: "Luke", done: false }
iterator.next(); // { value: "I am", done: false }
iterator.next(); // { value: "your", done: false }
iterator.next(); // { value: "father", done: true }

Как с этим работать?

Как с этим работать?

const velociraptor = dramaticTurnOfEvents();

for (const value of velociraptor) {
  console.log(value); // "Luke" → "I am" → "your" → "father"
}

Как с этим работать?

function* wrapIt() {
    yield* dramaticTurnOfEvents();

    return "NNNOOOOOOOOO!!1";
}
// sagas.js
import { takeLatest } from 'redux-saga';
import { call, put, select } from 'redux-saga/effects';

function* saga() {
    const actionsToWatch = [
        actionTypes.ADD_TODO,
        actionTypes.DELETE_TODO,
        actionTypes.CHECK_TODO
    ];

    yield* takeLatest(actionsToWatch, function* (action) {
         const { requestUrl } = yield select(state => state.urls);
         const { todos } = yield select(state => state.todos);
         yield put(actions.requestBegin());

         try {
             const response = yield call(axios.post, [requestUrl, { todos }]);
             yield put(actions.requestSuccess({ ...response.data }));
         } catch (e) {
             yield put(actions.handleError(e));
         }
    });
}
// sagas.js
import { takeLatest } from 'redux-saga';
import { call, put, select } from 'redux-saga/effects';

function* saga() {
    const actionsToWatch = [
        actionTypes.ADD_TODO,
        actionTypes.DELETE_TODO,
        actionTypes.CHECK_TODO
    ];

    yield* takeLatest(actionsToWatch, function* (action) {
         const { requestUrl } = yield select(state => state.urls);
         const { todos } = yield select(state => state.todos);
         yield put(actions.requestBegin());

         try {
             const response = yield call(axios.post, [requestUrl, { todos }]);
             yield put(actions.requestSuccess({ ...response.data }));
         } catch (e) {
             yield put(actions.handleError(e));
         }
    });
}
// sagas.js
import { takeLatest } from 'redux-saga';
import { call, put, select } from 'redux-saga/effects';

function* saga() {
    const actionsToWatch = [
        actionTypes.ADD_TODO,
        actionTypes.DELETE_TODO,
        actionTypes.CHECK_TODO
    ];

    yield* takeLatest(actionsToWatch, function* (action) {
         const { requestUrl } = yield select(state => state.urls);
         const { todos } = yield select(state => state.todos);
         yield put(actions.requestBegin());

         try {
             const response = yield call(axios.post, [requestUrl, { todos }]);
             yield put(actions.requestSuccess({ ...response.data }));
         } catch (e) {
             yield put(actions.handleError(e));
         }
    });
}
// sagas.js
import { takeLatest } from 'redux-saga';
import { call, put, select } from 'redux-saga/effects';

function* saga() {
    const actionsToWatch = [
        actionTypes.ADD_TODO,
        actionTypes.DELETE_TODO,
        actionTypes.CHECK_TODO
    ];

    yield* takeLatest(actionsToWatch, function* (action) {
         const { requestUrl } = yield select(state => state.urls);
         const { todos } = yield select(state => state.todos);
         yield put(actions.requestBegin());

         try {
             const response = yield call(axios.post, [requestUrl, { todos }]);
             yield put(actions.requestSuccess({ ...response.data }));
         } catch (e) {
             yield put(actions.handleError(e));
         }
    });
}
// sagas.js
import { takeLatest } from 'redux-saga';
import { call, put, select } from 'redux-saga/effects';

function* saga() {
    const actionsToWatch = [
        actionTypes.ADD_TODO,
        actionTypes.DELETE_TODO,
        actionTypes.CHECK_TODO
    ];

    yield* takeLatest(actionsToWatch, function* (action) {
         const { requestUrl } = yield select(state => state.urls);
         const { todos } = yield select(state => state.todos);
         yield put(actions.requestBegin());

         try {
             const response = yield call(axios.post, [requestUrl, { todos }]);
             yield put(actions.requestSuccess({ ...response.data }));
         } catch (e) {
             yield put(actions.handleError(e));
         }
    });
}
// sagas.js
import { takeLatest } from 'redux-saga';
import { call, put, select } from 'redux-saga/effects';

function* saga() {
    const actionsToWatch = [
        actionTypes.ADD_TODO,
        actionTypes.DELETE_TODO,
        actionTypes.CHECK_TODO
    ];

    yield* takeLatest(actionsToWatch, function* (action) {
         const { requestUrl } = yield select(state => state.urls);
         const { todos } = yield select(state => state.todos);
         yield put(actions.requestBegin());

         try {
             const response = yield call(axios.post, [requestUrl, { todos }]);
             yield put(actions.requestSuccess({ ...response.data }));
         } catch (e) {
             yield put(actions.handleError(e));
         }
    });
}

Тестирование

yield

saga effect

Тестирование

// generator.js
function* whosYourDaddy() {
  const result = yield "Who is your father?";

  console.log(result);
}

// client.js
const terminator = whosYourDaddy();
terminator.next("Darth Vader");
// result === "Darth Vader"

terminator.throw();

Тестирование

// generator.js
function* whosYourDaddy() {
  const result = yield "Who is your father?";

  console.log(result);
}

// client.js
const terminator = whosYourDaddy();
terminator.next("Darth Vader");
// result === "Darth Vader"

terminator.throw();

Тестирование

// generator.js
function* whosYourDaddy() {
  const result = yield "Who is your father?";

  console.log(result);
}

// client.js
const terminator = whosYourDaddy();
terminator.next("Darth Vader");
// result === "Darth Vader"

terminator.throw();

Тестирование

{
    CALL: {
      fn: axios.post,
      args: [requestUrl, { todos }]
    }
}

Тестирование

// sagas.test.js
const saga = fetchDataFlow(params);

it('should finish request', () => {
    expect(saga.next().value)
        .to
        .deep
        .equal(put(actions.requestSuccess({ todos: [] })));
    expect(saga.next().done).to.be.true;
});

Поддержка

13+

45+

55+

42+

10+

4+ (part)

6.5+

5.1+ (50%)

10+

Не все идеально

"use strict";

var _marked = [dramaticTurnOfEvents].map(regeneratorRuntime.mark);

function dramaticTurnOfEvents() {
    return regeneratorRuntime.wrap(function dramaticTurnOfEvents$(_context) {
        while (1) {
            switch (_context.prev = _context.next) {
                case 0:
                    _context.next = 2;
                    return "Luke";

                case 2:
                    _context.next = 4;
                    return "I am";

                case 4:
                    _context.next = 6;
                    return "your";

                case 6:
                    return _context.abrupt("return", "father");

                case 7:
                case "end":
                    return _context.stop();
            }
        }
    }, _marked[0], this);
}

Другие реализации

Спасибо!

Вопросы?

@vansosnin

bit.ly/uraljs_redux_saga

Saga

By vansosnin