async redux

andrei fidelman

fidelman.github.io

fidelman

our journey ~ 25min

async in a browser ~ 5min

async in code ~ 5min

async in redux ~ 15min

v8

heap

stack

main()
increment()
sum()
console.log()

call stack

code

const sum = (a, b) => a + b;


const increment = a => sum(a, 1);


const printIncrement = a => {

  const incremented = increment(a);

  console.log(`Incremented: ${incremented}`);
};

printIncrement(10);
main()
printIncrement()
increment()
sum()
console.log()

call stack

code

const foo = $.getSync('foo.com');

console.log('Hello World');

console.log(foo);
main()
$.getSync()
console.log('Hello World')
console.log(foo)

call stack

code

fetch('foo.com', { method: 'GET' })
    .then(foo => console.log(foo));

console.log('Hello World');
main()
fetch(callback)
console.log('Hello World')
callback

call stack

code

fetch('foo.com', { method: 'GET' })
    .then(foo => console.log(foo));

console.log('Hello World');
main()
fetch(callback)
console.log('Hello World')

console

event loop

web api

task queue

fetch(callback)
foo
"Hello world"
callback
callback

promise

promise

pending

fulfilled

rejected

wait

done

is an object that may produce a single value sometime in the future

promise

function wait(time) {
    return new Promise(function(resolve) {
        setTimeout(resolve, time);
    });
}

wait(3000)
    .then(function() {
        console.log('Hello');
    });
fetch('/article/promise/user.json', { method: 'GET' })
  .then(response => {
    console.log(response);
    const user = JSON.parse(response);
    return user;
  })
  .then(user => {
    console.log(user);
    return fetch(`https://api.github.com/users/${user.name}`, { method: 'GET' });
  })
  .then(githubUser => {
    console.log(githubUser);
    githubUser = JSON.parse(githubUser);

    let img = new Image();
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.appendChild(img);
  });

promise

Promise.reject()

Promise.resolve()

Promise.all()

Promise.race()

function generator

is a function which can suspend its execution, return an intermediate result and then resume it later, at an arbitrary moment in time

generator

function* generateSequence() {

    yield 1;

    yield 2;

    return 3;

}

const generator = generateSequence();

generator.next(); // { value: 1, done: false }
generator.next(); // { value: 2, done: false }
generator.next(); // { value: 3, done: true }
generator.next(); // { value: undefined, done: true }

generator

function* showUserAvatar() {
  const userInfo = yield fetch('/article/generator/user.json');
  const githubUserInfo = yield fetch(`https://api.github.com/users/${userInfo.name}`);

  ...

  yield new Promise(resolve => setTimeout(resolve, 3000));
}

execute(showUserAvatar);
function execute(generatorSequence, yieldValue) {
  const generator = generatorSequence();
  const next = generator.next(yieldValue);

  if (!next.done) {
    next.value.then(
      result => execute(generator, result),
      err => generator.throw(err)
    );
  } else {
    alert(next.value);
  }

}

async/await

is construction returns a promise

const f = async () => 1; // Promise

f().then(alert); // 1;
const promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve('done'), 1000);
});

const f = async () => {
    const result = await promise; // wait till the promise resolves

    alert(result); // "done!"
};

async/await

async function showUserAvatar() {
  const userInfo = await fetch('/article/generator/user.json');
  const githubUserInfo = await fetch(`https://api.github.com/users/${userInfo.name}`);

  ...

  await new Promise(resolve => setTimeout(resolve, 3000));
}

showUserAvatar();

async redux

view

action creators

middlewares

reducers

store

user interactions

actions

actions

new state

state

middleware

provides a third-party extension point between dispatching an action, and the moment it reaches a reducer

export default store => next => action => {

  if (action.type !== 'GET_PRODUCTS_FETCH') next(action);

  fetch(action.url, { method: 'GET' })
    .then((response) => {
      next({
        ...action,
        payload: response.payload
      });
    });
}
export default async store => next => action => {

  if (action.type !== 'GET_PRODUCTS_FETCH') next(action);

  const response = await fetch(action.url, { method: 'GET' });

  next({
    ...action,
    payload: response.payload
  });
}

redux-thunk

returns a function instead of an object

// action creator
export default (url) => {
  return (dispatch) => {

    dispatch({ type: 'GET_PRODUCTS_FETCH' });

    fetch(url, { method: 'GET' })
      .then((response) => {

        dispatch({
          type: 'GET_PRODUCTS_SUCCESS',
          payload: response.payload
        });

      });
  };
}
// action creator
export default (url) => {
  return async (dispatch) => {

    dispatch({ type: 'GET_PRODUCTS_FETCH' });

    const response = await fetch(url, { method: 'GET' });

    dispatch({
      type: 'GET_PRODUCTS_SUCCESS',
      payload: response.payload
    });
  };
}

redux-thunk

// store.js
const store = createStore(
  reducer,
  applyMiddleware(thunk.withExtraArgument(api))
)


// action.js
export default (url) => {
  return async (dispatch, getState, api) => {

    dispatch({ type: 'GET_PRODUCTS_FETCH' });

    const response = await fetch(url, { method: 'GET' });

    dispatch({
      type: 'GET_PRODUCTS_SUCCESS',
      payload: response.payload
    });
  };
}

redux-saga

is a library that aims to make side effects in React/Redux application easier

is a separate thread in the application that's solely responsible for side effects

is a Redux middleware

watcher/worker

initialisation

// store.js
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import rootSaga from './sagas'

const sagaMiddleware = createSagaMiddleware()

const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)

sagaMiddleware.run(rootSaga)

rootSaga

// sagas/index.js
import { all } from 'redux-saga/effects'

export default function* rootSaga() {
  yield all([
    watchIncrementAsync()
  ])
}
// sagas/increment.js

import { delay } from 'redux-saga'
import { put, takeEvery } from 'redux-saga/effects'

// worker
function* incrementAsync() {
  yield delay(1000)
  yield put({ type: 'INCREMENT' })
}

// watcher
export default function* watchIncrementAsync() {
  yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}

helpers

are used to wrap internal functions to spawn tasks when some specific actions are dispatched to the Store

// sagas/increment.js

import { takeEvery } from 'redux-saga/effects'

// watcher
export default function* watchIncrementAsync() {
  yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}
// sagas/increment.js

import { takeLatest } from 'redux-saga/effects'

// watcher
export default function* watchIncrementAsync() {
  yield takeLatest('INCREMENT_ASYNC', incrementAsync)
}

effects

is simply an object which contains some information to be interpreted by the middleware

// sagas/increment.js

import { takeEvery } from 'redux-saga/effects'
import Api from './path/to/api'

function* watchFetchProducts() {
  yield takeEvery('PRODUCTS_REQUESTED', fetchProducts)
}

function* fetchProducts() {
  const products = yield Api.fetch('/products')
  console.log(products)
}
// __test__/increment.js

const iterator = fetchProducts()
assert.deepEqual(iterator.next().value, ??)
// sagas/increment.js

import { takeEvery, call } from 'redux-saga/effects'
import Api from './path/to/api'

function* watchFetchProducts() {
  yield takeEvery('PRODUCTS_REQUESTED', fetchProducts)
}

function* fetchProducts() {
  const products = yield call(Api.fetch, '/products')
  console.log(products)
}
// __test__/increment.js

assert.deepEqual(
  iterator.next().value,
  call(Api.fetch, '/products'),
  "fetchProducts should yield an Effect call(Api.fetch, './products')"
)

put

call

take

fork

cancel

all

race

why is it useful?

🙂

status: 

😡

logged out

logged in

// action.js

export const login = (url, data) => {
  return async (dispatch) => {
    dispatch({ type: 'LOGIN_FETCH' });

    const response = await fetch(url, {method: 'post', data});

    if (response.success) {
      dispatch({
        type: 'LOGIN_FETCH_SUCCESS',
        token: response.token
      });
    } else {
      dispatch({
        type: 'LOGIN_FETCH_FAIL',
        errorMessage: response.errorMessage
      });
    }
  }
}

export const logout = (url, data) => {
  return async (dispatch) => {
    dispatch({ type: 'LOGOUT_FETCH' });

    const response = await fetch(url, {method: 'post', data});

    if (response.success) {
      dispatch({
        type: 'LOGOUT_FETCH_SUCCESS'
      });
    } else {
      dispatch({
        type: 'LOGOUT_FETCH_FAIL',
        response.errorMessage
      });
    }
  }
}

login

logout

why is it useful?

🙂

status: 

😡

logged out

// saga.js

import { take, call, put, cancelled } from 'redux-saga/effects'
import Api from '...'

function* authorize(user, password) {
  try {
    const token = yield call(Api.authorize, user, password)

    yield put({type: 'LOGIN_SUCCESS', token})

    yield call(Api.storeItem, {token})

    return token
  } catch(error) {
    yield put({type: 'LOGIN_ERROR', error})
  } finally {
    if (yield cancelled()) {
      // ... put special cancellation handling code here
    }
  }
}

export default function* loginFlow() {
  while (true) {
    const {user, password} = yield take('LOGIN_REQUEST')

    const task = yield fork(authorize, user, password)

    const action = yield take(['LOGOUT', 'LOGIN_ERROR'])

    if (action.type === 'LOGOUT') yield cancel(task)

    yield call(Api.clearItem, 'token')
  }
}

login

logout

😍

🙀

features

pulling future actions

make non-blocking calls

run tasks in parallel

start a race

make easy tests

use channels

connect to external I/O

make sequencing sagas

compose sagas

cancel task

more and more

advantages

easy to follow step by step

can allow fairly complicated flows

easy to write tests

sagas are composable

ACs are pure

isolated side-effect code

solid documentation

large functions selection

conclusion

JavaScript is single-threaded non-blocking  asynchronous concurrent language

know what are promises, generators, async/await

redux-thunk is for not complicated asynchronous logic

redux-saga can allow fairly complicated flows

q/a

Made with Slides.com