WarsawJS

We talk about JavaScript. Each month in Warsaw, Poland.

Speaker

Monika Glier

Handling Asynchronous Actions in React-Redux Apps

2017-04-12

@MonikaGlier

solar panels
sunrise

Asynchronous Actions

In programming, asynchronous events are those occurring independently of the main program flow. (...) executed in a non-blocking scheme, allowing the main program flow to continue processing.

Actions that are triggered by events outside of the direct control of the program, things that come from outside the program. Your code does not generate these events directly.

The actions that happen when they happen, not according to a predictable clock.

React

            
              componentDidMount() {
                fetch(url)
                .then(response => response.json())
                .then(json => {
                  this.setState({data: json})
                })
                .catch(error => doSomethingWith(error))
              }
            
        

Redux

redux

Request Actions

            
                const configRequest = (id) => {
                  return {
                    type: 'CONFIG_REQUEST',
                    payload: { id }
                  }
                }

                // 'Pending' action => fetching: true, fetched: false
            
        

            
                const configSuccess = (json) => {
                  return {
                    type: 'CONFIG_SUCCESS',
                    payload: { json }
                  }
                }
                // 'Success' action => fetching: false, fetched: true


                const configError = (error) => {
                  return {
                    type: 'CONFIG_ERROR',
                    payload: { error }
                  }
                }
                // ‘Failure' action => fetching: false, fetched: false

            
        

redux-thunk

            
                // store.js

                import { createStore } from 'redux'
                const store = createStore(reducer)

                ***

                import { applyMiddleware, createStore } from 'redux'
                import thunk from "redux-thunk"
                const middleware = applyMiddleware(thunk)
                const store = createStore(reducer, middleware)
            
        

redux-thunk async action creator

            
                const fetchConfig = ({ id }) => {
                  return (dispatch) => {
                    dispatch(configRequest(id))
                    return api.fetchConfiguration(id)
                    .then(({ json }) => dispatch(configSuccess(json)))
                    .catch((error) => dispatch(configError(error))
                  }
                }
            
        

...with extra promise inside

            
                const fetchConfig = ({ id }) => {
                  return (dispatch) => {
                    dispatch(configRequest(id))
                    return api.fetchConfiguration(id)
                    .then(({ json }) => {
                      new Deserializer({ keyForAttribute: "underscore_case" })
                      .deserialize(json)
                      .then((deviceConfig) => dispatch(configSuccess(deviceConfig)))
                    })
                    .catch((error) => dispatch(configurationError(error))
                  }
                }
            
        

redux-saga

            
                // store.js

                import { createStore } from 'redux'
                const store = createStore(reducer)

                ***

                import { applyMiddleware, createStore } from 'redux'
                import createSagaMiddleware from 'redux-saga'
                const sagaMiddleware = createSagaMiddleware()
                const middleware = applyMiddleware(sagaMiddleware)
                const store = createStore(reducer, middleware)
                sagaMiddleware.run(rootSaga)
            
        
saga

rootSaga

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

                export function* watchAsync() {
                  yield takeEvery('CONFIG_REQUEST', configAsync)
                }

                export default function* rootSaga() {
                  yield [
                    watchAsync(),
                  ]
                }
            
        

redux-saga async action generator

            
                export function* configAsync(action) {
                  try {
                    const { json } = yield call(fetchConfig, action.payload.id)
                    yield put(configSuccess(json))
                  } catch (error) {
                    yield put(configError(error))
                  }
                }
            
        

...with extra promise inside

            
                export function* configAsync(action) {
                  try {
                    const { json } = yield call(fetchConfig, action.payload.id)
                    const config = yield call(deserializeConfig, json)
                    yield put(configSuccess(config))
                  } catch (error) {
                    yield put(configError(error))
                  }
                }
            
        
            
                describe('on response ok', () => {
                  const configSaga = saga.configAsync(exampleConfigRequestAction)
                  it('calls fetch-config with correct params', () => {
                    const effect = configSaga.next()
                    expect(effect.done).toEqual(false)
                    expect(effect.value).toEqual(call(fetchConfig, id))
                  })
                  it('puts config-success-action and passes json', () => {
                    const effect = configSaga.next({ response: responseOk, json: exampleJson })
                    expect(effect.done).toEqual(false)
                    expect(effect.value).toEqual(put(configSuccess(exampleJson)))
                  })
                  it('is done', () => {
                    const effect = configSaga.next()
                    expect(effect.done).toEqual(true)
                    expect(effect.value).toEqual(undefined)
                  })
                })
            
        
            
                describe('on error response', () => {
                  const configSaga = saga.configAsync(exampleConfigRequestAction)
                  it('calls fetch-config with correct params', () => {
                    const effect = configSaga.next()
                    expect(effect.done).toEqual(false)
                    expect(effect.value).toEqual(call(fetchConfig, id))
                  })
                  it('puts config-error-action and passes error', () => {
                    const effect = configSaga.throw('error')
                    expect(effect.done).toEqual(false)
                    expect(effect.value).toEqual(put(configError('error')))
                  })
                  it('is done', () => {
                    const effect = configSaga.next()
                    expect(effect.done).toEqual(true)
                    expect(effect.value).toEqual(undefined)
                  })
                })
            
        

redux-thunk tests

THUNKS SAGAS
...imagine solar power plant Solar Panels
...imagine a sandstorm Sandstorm
Boom

redux-saga - conclusions

Resources

Thank you

See you next month at WarsawJS