Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 })   
    }) 
}
like image 471
GeorgePal Avatar asked Sep 20 '19 18:09

GeorgePal


People also ask

What does dispatching an action mean?

In a store like this dispatch is how actions get sent. You might also think of it as "firing and action" or "sending an action". The way the store works is very well defined. You cannot just set a new value in the store, you must tell the store that some action has happened. That's what "dispatching" is.

How do you dispatch actions in Saga?

Create a plain JavaScript Object to instruct the middleware that we need to dispatch some action, and let the middleware perform the real dispatch. This way we can test the Generator's dispatch in the same way: by inspecting the yielded Effect and making sure it contains the correct instructions.

How do you dispatch actions in a functional component?

There are two ways to dispatch actions from functional components: Using mapDispatachToProps function with connect higher order component, same as in class based components. Using useDispatch hook provided by react-redux . If you want to use this hook, then you need to import it from the react-redux package.

How do I dispatch action in reducer?

Dispatching an action within a reducer is an anti-pattern. Your reducer should be without side effects, simply digesting the action payload and returning a new state object. Adding listeners and dispatching actions within the reducer can lead to chained actions and other side effects.


Video Answer


1 Answers

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).

like image 94
apachuilo Avatar answered Oct 07 '22 13:10

apachuilo