THE EVOLUTION
OF REDUX ACTION CREATORS
GEORGE BUKHANOV
@NothernEyes
northerneyes
Redux is a predictable state container for JavaScript apps.
Action is just a plain object
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
Reducer is a pure function
function todoApp(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
})
default:
return state
}
}
WHAT IS ACTION CREATOR?
Action creator
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
WHAT ABOUT ASYNC ACTIONS?
It provides a third-party extension point between dispatching
an action, and the moment it reaches the reducer.
REDUX MIDDLEWARE
Redux-thunk
export default function thunkMiddleware({ dispatch, getState }) {
return next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};
}
Action creator with redux-thunk
function increment() {
return {
type: INCREMENT_COUNTER
};
}
function incrementAsync() {
return dispatch => {
setTimeout(() => {
// Yay! Can invoke sync or async actions with `dispatch`
dispatch(increment());
}, 1000);
};
}
WHAT ABOUT TESTING?
OUR ACTION CREATORS ARE NOT PURE FUNCTIONS
THE ANSWER IS
DEPENDENCY INJECTION
Middleware with dependency injection
export default function injectDependencies(dependencies) {
return ({dispatch, getState}) => next => action => {
if (typeof action !== 'function') return next(action);
return action({dispatch, getState, ...dependencies});
};
}
Action creator with DI
export function registration(data) {
return ({dispatch, api, history, analytics, cookie}) => {
dispatch({type: authConstants.REGISTRATION_PENDING});
return api.register(data).then(res => {
updateAnalytics(analytics, res, true);
saveUserCookie(res, cookie);
analytics.track('Registration started');
dispatch({type: authConstants.REGISTRATION_SUCCESS});
const link = '/search';
history.push(link);
}).catch(onError(authConstants.REGISTRATION_ERROR, dispatch));
};
}
YEAH! THEY ARE PURE FUNCTIONS
BUT IT IS NOT ENOUGH, WHY?
Tests are still complecated
We have some mess in components
etc...
function() {
updateSearchPage()({dispatch, getState: buildState(), api, cookie});
expect(dispatch.calledOnce).to.be.true;
expect(calledWithActions(
dispatch.getCall(0).args,
APPLIED_FILTERS_CHANGED,
GET_NOTICES_SUCCESS
)).to.be.true;
};
function onHandlePress () {
this.props.dispatch({type: 'SHOW_WAITING_MODAL'})
this.props.dispatch(createRequest())
}
REDUX-SAGA
The most elegant way to write complecated action creators
Look at this beautiful code
export function* authFlow() {
while(true) {
yield take(USER_AUTH_CHECK);
yield fork(authenticate);
const {user, token} = yield take(USER_AUTH_SUCCESS);
Session.save(user, auth);
yield put(redirectTo('/'));
const action = yield take(USER_SIGN_OUT);
Session.clear();
yield put(redirectTo('/'));
}
}
HOW IT WORKS?
GENERATORS
Generators are Functions with bene ts.
function* idMaker(){
var index = 0;
while(true)
yield index++;
}
var gen = idMaker();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
co - generator based control ow
var fn = co.wrap(function* (val) {
return yield Promise.resolve(val);
});
fn(true).then(function (val) {
});
LET'S GET BACK TO REDUX-SAGA
Simple example What happens
here?
select part of the state
call the api method
put an action
export function* checkout() {
try {
const cart = yield select(getCart);
yield call(api.buyProducts, cart);
yield put(actions.checkoutSuccess(cart));
} catch(error) {
yield put(actions.checkoutFailure(error));
}
}
Easy to test
test('checkout Saga test', function (t) {
const generator = checkout()
let next = generator.next()
t.deepEqual(next.value, select(getCart),
"must select getCart"
)
next = generator.next(cart)
t.deepEqual(next.value, call(api.buyProducts, cart),
"must call api.buyProducts(cart)"
)
next = generator.next()
t.deepEqual(next.value, put(actions.checkoutSuccess(cart)),
"must yield actions.checkoutSuccess(cart)"
)
t.end()
})
SAGAS CAN BE DAEMONS
Endless loop, is listenertake
export function* watchCheckout() {
while(true) {
yield take(actions.CHECKOUT_REQUEST)
yield call(checkout)
}
}
Root Saga
export default function* root() {
yield [
fork(watchCheckout)
]
}
REDUX-SAGA PATTERNS
can be useful to handle AJAX requests where we
want to only have the response to the latest request.
takeLatest
function* takeLatest(pattern, saga, ...args) {
let lastTask
while(true) {
const action = yield take(pattern)
if(lastTask)
// cancel is no-op if the task has alerady terminated
yield cancel(lastTask)
lastTask = yield fork(saga, ...args.concat(action))
}
}
allows multiple saga tasks to be forked
concurrently.
takeEvery
function* takeEvery(pattern, saga, ...args) {
while (true) {
const action = yield take(pattern)
yield fork(saga, ...args.concat(action))
}
}
FIN
@NothernEyes
northerneyes

The evolution of redux action creators

  • 1.
    THE EVOLUTION OF REDUXACTION CREATORS GEORGE BUKHANOV @NothernEyes northerneyes
  • 4.
    Redux is apredictable state container for JavaScript apps.
  • 6.
    Action is justa plain object { type: ADD_TODO, text: 'Build my first Redux app' }
  • 7.
    Reducer is apure function function todoApp(state = initialState, action) { switch (action.type) { case ADD_TODO: return Object.assign({}, state, { todos: [ ...state.todos, { text: action.text, completed: false } ] }) default: return state } }
  • 8.
  • 9.
    Action creator function addTodo(text){ return { type: ADD_TODO, text } }
  • 10.
  • 11.
    It provides athird-party extension point between dispatching an action, and the moment it reaches the reducer. REDUX MIDDLEWARE
  • 12.
    Redux-thunk export default functionthunkMiddleware({ dispatch, getState }) { return next => action => { if (typeof action === 'function') { return action(dispatch, getState); } return next(action); }; }
  • 13.
    Action creator withredux-thunk function increment() { return { type: INCREMENT_COUNTER }; } function incrementAsync() { return dispatch => { setTimeout(() => { // Yay! Can invoke sync or async actions with `dispatch` dispatch(increment()); }, 1000); }; }
  • 14.
  • 15.
    OUR ACTION CREATORSARE NOT PURE FUNCTIONS
  • 16.
  • 17.
    Middleware with dependencyinjection export default function injectDependencies(dependencies) { return ({dispatch, getState}) => next => action => { if (typeof action !== 'function') return next(action); return action({dispatch, getState, ...dependencies}); }; }
  • 18.
    Action creator withDI export function registration(data) { return ({dispatch, api, history, analytics, cookie}) => { dispatch({type: authConstants.REGISTRATION_PENDING}); return api.register(data).then(res => { updateAnalytics(analytics, res, true); saveUserCookie(res, cookie); analytics.track('Registration started'); dispatch({type: authConstants.REGISTRATION_SUCCESS}); const link = '/search'; history.push(link); }).catch(onError(authConstants.REGISTRATION_ERROR, dispatch)); }; }
  • 19.
    YEAH! THEY AREPURE FUNCTIONS
  • 20.
    BUT IT ISNOT ENOUGH, WHY?
  • 21.
    Tests are stillcomplecated We have some mess in components etc... function() { updateSearchPage()({dispatch, getState: buildState(), api, cookie}); expect(dispatch.calledOnce).to.be.true; expect(calledWithActions( dispatch.getCall(0).args, APPLIED_FILTERS_CHANGED, GET_NOTICES_SUCCESS )).to.be.true; }; function onHandlePress () { this.props.dispatch({type: 'SHOW_WAITING_MODAL'}) this.props.dispatch(createRequest()) } REDUX-SAGA
  • 22.
    The most elegantway to write complecated action creators Look at this beautiful code export function* authFlow() { while(true) { yield take(USER_AUTH_CHECK); yield fork(authenticate); const {user, token} = yield take(USER_AUTH_SUCCESS); Session.save(user, auth); yield put(redirectTo('/')); const action = yield take(USER_SIGN_OUT); Session.clear(); yield put(redirectTo('/')); } }
  • 23.
  • 24.
  • 25.
    Generators are Functionswith bene ts. function* idMaker(){ var index = 0; while(true) yield index++; } var gen = idMaker(); console.log(gen.next().value); // 0 console.log(gen.next().value); // 1 console.log(gen.next().value); // 2
  • 26.
    co - generatorbased control ow var fn = co.wrap(function* (val) { return yield Promise.resolve(val); }); fn(true).then(function (val) { });
  • 27.
    LET'S GET BACKTO REDUX-SAGA
  • 28.
    Simple example Whathappens here? select part of the state call the api method put an action export function* checkout() { try { const cart = yield select(getCart); yield call(api.buyProducts, cart); yield put(actions.checkoutSuccess(cart)); } catch(error) { yield put(actions.checkoutFailure(error)); } }
  • 29.
    Easy to test test('checkoutSaga test', function (t) { const generator = checkout() let next = generator.next() t.deepEqual(next.value, select(getCart), "must select getCart" ) next = generator.next(cart) t.deepEqual(next.value, call(api.buyProducts, cart), "must call api.buyProducts(cart)" ) next = generator.next() t.deepEqual(next.value, put(actions.checkoutSuccess(cart)), "must yield actions.checkoutSuccess(cart)" ) t.end() })
  • 30.
    SAGAS CAN BEDAEMONS
  • 31.
    Endless loop, islistenertake export function* watchCheckout() { while(true) { yield take(actions.CHECKOUT_REQUEST) yield call(checkout) } }
  • 32.
    Root Saga export defaultfunction* root() { yield [ fork(watchCheckout) ] }
  • 33.
  • 34.
    can be usefulto handle AJAX requests where we want to only have the response to the latest request. takeLatest function* takeLatest(pattern, saga, ...args) { let lastTask while(true) { const action = yield take(pattern) if(lastTask) // cancel is no-op if the task has alerady terminated yield cancel(lastTask) lastTask = yield fork(saga, ...args.concat(action)) } }
  • 35.
    allows multiple sagatasks to be forked concurrently. takeEvery function* takeEvery(pattern, saga, ...args) { while (true) { const action = yield take(pattern) yield fork(saga, ...args.concat(action)) } }
  • 36.