Search code examples
javascriptreactjsreact-reduxredux-thunkdrizzle

Dispatch actions the proper way


Please, check the Edit

I'm trying to implement sagas in my app.

Right now I am fetching the props in a really bad way. My app consists mainly on polling data from other sources.

Currently, this is how my app works:

I have containers which have mapStateToProps, mapDispatchToProps.

const mapStateToProps = state => {
  return {
    someState: state.someReducer.someReducerAction,
  };
};

const mapDispatchToProps = (dispatch) => {
  return bindActionCreators({someAction, someOtherAction, ...}, dispatch)
};

const something = drizzleConnect(something, mapStateToProps, mapDispatchToProps);

export default something;

and then, I have actions, like this:

import * as someConstants from '../constants/someConstants';

export const someFunc = (someVal) => (dispatch) => {
    someVal.methods.someMethod().call().then(res => {
        dispatch({
            type: someConstants.FETCH_SOMETHING,
            payload: res
        })

    })
}

and reducers, like the below one:

export default function someReducer(state = INITIAL_STATE, action) {
    switch (action.type) {
        case types.FETCH_SOMETHING:
            return ({
                ...state,
                someVar: action.payload
            });

I combine the reducers with redux's combineReducers and export them as a single reducer, which, then, I import to my store.

Because I use drizzle, my rootSaga is this:

import { all, fork } from 'redux-saga/effects'
import { drizzleSagas } from 'drizzle'

export default function* root() {
  yield all(
    drizzleSagas.map(saga => fork(saga)),
  )
}

So, now, when I want to update the props, inside the componentWillReceiveProps of the component, I do: this.props.someAction()

Okay, it works, but I know that this is not the proper way. Basically, it's the worst thing I could do.

So, now, what I think I should do:

Create distinct sagas, which then I'll import inside the rootSaga file. These sagas will poll the sources every some predefined time and update the props if it is needed.

But my issue is how these sagas should be written.

Is it possible that you can give me an example, based on the actions, reducers and containers that I mentioned above?

Edit:

I managed to follow apachuilo's directions.

So far, I made these adjustments:

The actions are like this:

export const someFunc = (payload, callback) => ({
            type: someConstants.FETCH_SOMETHING_REQUEST,
            payload,
            callback
})

and the reducers, like this:

export default function IdentityReducer(state = INITIAL_STATE, {type, payload}) {
    switch (type) {
        case types.FETCH_SOMETHING_SUCCESS:
            return ({
                ...state,
                something: payload,
            });
...

I also created someSagas:

...variousImports

import * as apis from '../apis/someApi'

function* someHandler({ payload }) {
    const response = yield call(apis.someFunc, payload)

    response.data
        ? yield put({ type: types.FETCH_SOMETHING_SUCCESS, payload: response.data })
        : yield put({ type: types.FETCH_SOMETHING_FAILURE })
}

export const someSaga = [
    takeLatest(
        types.FETCH_SOMETHING_REQUEST,
        someHandler
    )
]

and then, updated the rootSaga:

import { someSaga } from './sagas/someSagas'

const otherSagas = [
  ...someSaga,
]

export default function* root() {
  yield all([
    drizzleSagas.map(saga => fork(saga)),
    otherSagas
  ])
}

Also, the api is the following:

export const someFunc = (payload) => {
    payload.someFetching.then(res => {
        return {data: res}
    }) //returns 'data' of undefined but just "return {data: 'something'} returns that 'something'

So, I'd like to update my questions:

  1. My APIs are depended to the store's state. As you may understood, I'm building a dApp. So, Drizzle (a middleware that I use in order to access the blockchain), needs to be initiated before I call the APIs and return information to the components. Thus,

    a. Trying reading the state with getState(), returns me empty contracts (contracts that are not "ready" yet) - so I can't fetch the info - I do not like reading the state from the store, but...

    b. Passing the state through the component (this.props.someFunc(someState), returns me Cannot read property 'data' of undefined The funny thing is that I can console.log the state (it seems okay) and by trying to just `return {data: 'someData'}, the props are receiving the data.

  2. Should I run this.props.someFunc() on, for e.g., componentWillMount()? Is this the proper way to update the props?

Sorry for the very long post, but I wanted to be accurate.

Edit for 1b: Uhh, so many edits :) I solved the issue with the undefined resolve. Just had to write the API like this:

export function someFunc(payload)  {

    return payload.someFetching.then(res => {
            return ({ data: res })   
    }) 
}

Solution

  • I don't want to impose the pattern I use, but I've used it with success for awhile in several applications (feedback from anyone greatly appreciated). Best to read around and experiment to find what works best for you and your projects.

    Here is a useful article I read when coming up with my solution. There was another, and if I can find it -- I'll add it here.

    https://medium.com/@TomasEhrlich/redux-saga-factories-and-decorators-8dd9ce074923

    This is the basic setup I use for projects. Please note my use of a saga util file. I do provide an example of usage without it though. You may find yourself creating something along the way to help you reducing this boilerplate. (maybe even something to help handle your polling scenario).

    I hate boilerplate so much. I even created a tool I use with my golang APIs to auto-generate some of this boilerplate by walking the swagger doc/router endpoints.

    Edit: Added container example.

    example component

    import React, { Component } from 'react'
    
    import { connect } from 'react-redux'
    import { bindActionCreators } from 'redux'
    import { getResource } from '../actions/resource'
    
    const mapDispatchToProps = dispatch =>
      bindActionCreators(
        {
          getResource
        },
        dispatch
      )
    
    class Example extends Component {
      handleLoad = () => {
        this.props.getResource({
          id: 1234
        })
      }
    
      render() {
        return <button onClick={this.handleLoad}>Load</button>
      }
    }
    
    export default connect(
      null,
      mapDispatchToProps
    )(Example)
    

    example action/resource.js

    import { useDispatch } from 'react-redux'
    
    const noop = () => {}
    const empty = []
    
    export const GET_RESOURCE_REQUEST = 'GET_RESOURCE_REQUEST'
    export const getResource = (payload, callback) => ({
      type: GET_RESOURCE_REQUEST,
      payload,
      callback,
    })
    
    // I use this for projects with hooks!
    export const useGetResouceAction = (callback = noop, deps = empty) => {
      const dispatch = useDispatch()
    
      return useCallback(
        payload =>
          dispatch({ type: GET_RESOURCE_REQUEST, payload, callback }),
        // eslint-disable-next-line react-hooks/exhaustive-deps
        [dispatch, ...deps]
      )
    }
    

    Fairly basic redux action file.

    example reducers/resource.js

    export const GET_RESOURCE_SUCCESS = 'GET_RESOURCE_SUCCESS'
    
    const initialState = {
      resouce: null
    }
    
    export default (state = initialState, { type, payload }) => {
      switch (type) {
        case GET_RESOURCE_SUCCESS: {
          return {
            ...state,
            resouce: payload.Data,
          }
        }
    }
    

    Fairly standard reducer pattern - NOTE the use of _SUCCESS instead of _REQUEST here. That's important.

    example saga/resouce.js

    import { takeLatest } from 'redux-saga/effects'
    
    import { GET_RESOUCE_REQUEST } from '../actions/resource'
    
    // need if not using the util
    import { GET_RESOURCE_SUCCESS } from '../reducers/resource'
    
    import * as resouceAPI from '../api/resource'
    
    import { composeHandlers } from './sagaHandlers'
    
    // without the util
    function* getResourceHandler({ payload }) {
        const response = yield call(resouceAPI.getResouce, payload);
    
        response.data
          ? yield put({ type: GET_RESOURCE_SUCCESS, payload: response.data })
          : yield put({
              type: "GET_RESOURCE_FAILURE"
            });
      }
    
    export const resourceSaga = [
      // Example that uses my util
      takeLatest(
        GET_RESOUCE_REQUEST,
        composeHandlers({
          apiCall: resouceAPI.getResouce
        })
      ),
      // Example without util
      takeLatest(
        GET_RESOUCE_REQUEST,
        getResourceHandler
      )
    ]
    

    Example saga file for some resource. This is where I wire up the api call with the reducer call in array per endpoint for the reosurce. This then gets spread over the root saga. Sometimes you may want to use takeEvery instead of takeLatest -- all depends on the use case.

    example saga/index.js

    import { all } from 'redux-saga/effects'
    
    import { resourceSaga } from './resource'
    
    export const sagas = [
      ...resourceSaga,
    ]
    
    export default function* rootSaga() {
      yield all(sagas)
    }
    

    Simple root saga, looks a bit like a root reducer.

    util saga/sagaHandlers.js

    export function* apiRequestStart(action, apiFunction) {
      const { payload } = action
    
      let success = true
      let response = {}
      try {
        response = yield call(apiFunction, payload)
      } catch (e) {
        response = e.response
        success = false
      }
    
      // Error response
      // Edit this to fit your needs
      if (typeof response === 'undefined') {
        success = false
      }
    
      return {
        action,
        success,
        response,
      }
    }
    
    export function* apiRequestEnd({ action, success, response }) {
      const { type } = action
      const matches = /(.*)_(REQUEST)/.exec(type)
      const [, requestName] = matches
    
      if (success) {
        yield put({ type: `${requestName}_SUCCESS`, payload: response })
      } else {
        yield put({ type: `${requestName}_FAILURE` })
      }
    
      return {
        action,
        success,
        response,
      }
    }
    
    // External to redux saga definition -- used inside components
    export function* callbackHandler({ action, success, response }) {
      const { callback } = action
      if (typeof callback === 'function') {
        yield call(callback, success, response)
      }
    
      return action
    }
    
    export function* composeHandlersHelper(
      action,
      {
        apiCall = () => {}
      } = {}
    ) {
      const { success, response } = yield apiRequestStart(action, apiCall)
    
      yield apiRequestEnd({ action, success, response })
    
      // This callback handler is external to saga
      yield callbackHandler({ action, success, response })
    }
    
    export function composeHandlers(config) {
      return function*(action) {
        yield composeHandlersHelper(action, config)
      }
    }
    

    This is a very shortened version of my saga util handler. It can be a lot to digest. If you want the full version, I'll see what I can do. My full one handles stuff like auto-generating toast on api success/error and reloading certain resources upon success. Have something for handling file downloads. And another thing for handling any weird internal logic that might have to happen (rarely use this).