Иван Соснин, СКБ Контур
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
Saga
- 990