How do I handle multiple calls in one thunk function?

10,486

Solution 1

Assuming your request do not depend on each other, I would suggest keeping each action separate. Possible benefits of that approach:

  • Easier debugging
  • Easier testing
  • Closer to single responsibility principle

Suggested implementation

actions:

const fetchCustomers = () => dispatch => {
  try {
    axios.get(`${settings.hostname}/customers`)
      .then(res => {
           dispatch(setCustomers(res.json()))
       })

  } catch(err) => console.log('Error', err)
}

const fetchEvents = () => dispatch => {
  try {
    axios.get(`${settings.hostname}/events`)
      .then(res => {
           dispatch(setEvents(res.json()))
       })

  } catch(err) => console.log('Error', err)
}

const fetchLocks = () => dispatch => {
  try {
    axios.get(`${settings.hostname}/locks`)
      .then(res => {
           dispatch(setLocks(res.json()))
       })
  } catch(err) => console.log('Error', err)
}

Doors component snippet:

import * as actionCreators from './actions.js' 
import { bindActionCreators } from 'redux'
/*...*/
componentDidMount() {
    this.props.fetchCustomers();
    this.props.fetchEvents();
    this.props.fecthLocks();
  }

const mapStateToProps = state => {
  return {
    customers: state.doors.customers,
    events: state.doors.events,
    locks: state.doors.locks
  }
}

const mapDispatchToProps = dispatch => {
  return bindActionCreators(actionCreators, dispatch);
}

export default connect(mapStateToProps, mapDispatchToProps)(Doors) 

EDIT: added example reducer and adopted mapStateToProps accordingly. In this reducer instance doors is the this container's state and holds events, locks and customers:

export function events(state = initialState, action) {
    switch (action.type) {
        case 'EVENTS_FETCH_SUCCESS':
            return Object.assign({}, state, {events: action.events});
        case 'CUSTOMERS_FETCH_SUCCESS':
            return Object.assign({}, state, {customers: action.customers});
        case 'LOCKS_FETCH_SUCCESS':
            return Object.assign({}, state, {locks: action.locks});
        default:
            return state;
    }
}

Now requests are executed in parallel and they fail independently.

Solution 2

Redux

Your events reducer should look more like:

export function events(state = initialState, action) {
    switch (action.type) {
        case 'EVENTS_FETCH_DATA_SUCCESS':
            return Object.assign({}, state, {events: action.events});
        default:
            return state;
    }
}

Or if you have the object spread transform, you can do:

case 'EVENTS_FETCH_DATA_SUCCESS':
    return {...state, events: action.events};

You should always make your reducers pure functions, and one of the requirements of a pure function is that it doesn't modify its inputs directly. You need to return a new state object, with your changes merged in.

After making this change, you'll want a similar reducer for your other actions (customers, locks).


You also have syntax errors in your thunk:

1) .then(res) => res.json() should be:

.then(res => res.json())


2) catch(err) => console.log('Error', err) should be:

catch(err) {
  console.error('Error', err)
}


Back to React

You're already on the right track in your component. I imagine you have components Customers and Locks to go along with your Doors component.

But why is it that you want to group your API requests and perform them sequentially? Unless each subsequent request needs data from the previous response, you can do them in parallel and reduce latency.

Then, since your reducers are writing the response to the state, and your mapStateToProps, well... maps the state to your props, you can simply read from this.props.doors inside your render method. If you want to show a loader while you're waiting for your props to populate with actual values, you can add some conditional logic like:

render() {
  const { doors } = this.props;

  return (
    <div className="Doors">
      {doors === null
        ? this.renderLoader()
        : this.renderDoors()
      }
    </div>
  );
}

renderLoader() {
  return <Loader/>;
}

renderDoors() {
  const { doors } = this.props;

  return doors.map(door => (
    <Door key={door.id}/>
  ));
}

Now when Doors is rendered, in componentDidMount you can call your original fetchData and your component will automatically be re-rendered when the doors prop changes. The same goes for each other component; when they mount, they request new data, and when the data arrives, your thunk dispatches a store update, and your component's props change in response to that update.


But just for fun

Let's say your API flow is actually complicated enough that it requires each API request to know some data from the previous request, like if you're fetching a customer's favorite brand of door, but the fetchCustomer endpoint only returns you favoriteDoorID, so you have to call fetchDoor(favoriteDoorID).

Now you have a real use case for your combined thunk, which would look something like:

const fetchFavoriteDoorForCustomer = (customerID) => async dispatch => {
  try {
    const customer = await axios.get(`${settings.hostname}/customer/${customerID}`)
      .then(res => res.json());

    const doorID = customer.favoriteDoorID;
    const favoriteDoor = await axios.get(`${settings.hostname}/door/${doorID}`)
      .then(res => res.json());

    dispatch(receivedFavoriteDoor(favoriteDoor));
  } catch(err) {
    console.error('Error', err);
  }
}

You would then create a case in one of your reducers, listening for the action type RECEIVED_FAVORITE_DOOR or some such, which would merge it into the state like written in my first section.

And finally the component which requires this data would be subscribed to the store using connect, with the appropriate mapStateToProps function:

const mapStateToProps = state => {
  return {
    favoriteDoor: state.favoriteDoor
  }
}

So now you can just read from this.props.favoriteDoor, and you're good to go.


Hope that helps.

Solution 3

Best practices is to break down state (redux terminology) into small pieces .

Thus, instead of having a an array (state structure) of objects cocktail [{event-object}, {customer-object}, {event-object},...so on] you will have array for each data , then : reducer for each data.

export default combineReducers({
  events,
  customers,
  locks,
}) 

If we agree on this architecture, your dispatchers , actions.. so on will follow this architecture .

Then :

actions.js

export function getEvents() {
   return (dispatch) => axios.get(`${settings.hostname}/events`)
      .then(res) => res.json())
      .then(events => dispatch(setEvents(events)))
 }
 // export function getCustomers ... so on

Then , in you component , you will need to dispatch the three calls sequentially unless there is dependency among them :

import {getEvents, getCustomers, ... so on} from 'actions';
componentDidMount() {
   this.props.dispatch(getEvents())  // you can use `connect` instead of `this.props.dispatch` through `mapDisptachToProps`
   this.props.dispatch(getCustomers())
   // .. so on
}
 
Share:
10,486
Martin Nordström
Author by

Martin Nordström

Just trying to learn how to code and later take over the world. My LinkedIn profile

Updated on June 07, 2022

Comments

  • Martin Nordström
    Martin Nordström almost 2 years

    I don’t know if everybody has read this: https://medium.com/@stowball/a-dummys-guide-to-redux-and-thunk-in-react-d8904a7005d3?source=linkShare-36723b3116b2-1502668727 but it basically teaches you how to handle one API requests with redux and redux thunk. It’s a great guide, but I wonder what if my React component is having more than just one get request to a server? Like this:

    componentDidMount() {
        axios.get('http://localhost:3001/customers').then(res => {
          this.setState({
            res,
            customer: res.data
          })
        })
    
        axios.get('http://localhost:3001/events').then(res => {
          this.setState({
            res,
            event: res.data
          })
        })
    
        axios.get('http://localhost:3001/locks').then(res => {
          this.setState({
            res,
            lock: res.data
          })
        })
      }
    

    I've been googling like crazy and I think I've made some progress, my action creator currently looks like this (don't know if its 100% correct):

    const fetchData = () => async dispatch => {
      try {
        const customers = await axios.get(`${settings.hostname}/customers`)
          .then(res) => res.json()
    
        const events = await axios.get(`${settings.hostname}/events`)
          .then(res) => res.json()
    
        const locks = await axios.get(`${settings.hostname}/locks`)
          .then(res) => res.json()
    
        dispatch(setCustomers(customers))
        dispatch(setEvents(events))
        dispatch(setLocks(locks))
      } catch(err) => console.log('Error', err)
    }
    

    So the next step is to create your reducers, I just made one:

    export function events(state = initialState, action) {
        switch (action.type) {
            case 'EVENTS_FETCH_DATA_SUCCESS':
                return action.events;
            default:
                return state;
        }
    }
    

    Here's my problem:

    I don't know how to handle this inside of my component now. If you follow the article ( https://medium.com/@stowball/a-dummys-guide-to-redux-and-thunk-in-react-d8904a7005d3?source=linkShare-36723b3116b2-1502668727) it will end up like this:

    componentDidMount() {
      this.props.fetchData('http://localhost:3001/locks')
    }
    

    And

        Doors.propTypes = {
      fetchData: PropTypes.func.isRequired,
      doors: PropTypes.object.isRequired
    }
    
    const mapStateToProps = state => {
      return {
        doors: state.doors
      }
    }
    
    const mapDispatchToProps = dispatch => {
      return {
        fetchData: url => dispatch(doorsFetchData(url))
      }
    }
    
    export default connect(mapStateToProps, mapDispatchToProps)(Doors)
    

    My question

    So my question is how should I handle my multiple get requests inside of my component now? Sorry if this questions seems lazy, but I really can't figure it out and I've really been trying to.

    All help is super appreciated!!

  • Martin Nordström
    Martin Nordström almost 7 years
    How would the reducer look like? You wont have to write it if you don't want, but I would really appreciate it!
  • Kristupas Repečka
    Kristupas Repečka almost 7 years
    Edited. Argument could be made that you should split state into separate reducers. But in my opinion it solely depends on use case.
  • Martin Nordström
    Martin Nordström almost 7 years
    I understand! You are very good my man. Sorry if I ask too many questions, but why is setCustomers() not defined here? dispatch(setCustomers(res.json())) :O
  • Kristupas Repečka
    Kristupas Repečka over 6 years
    Well, it was assumed you have these actions defined. Do dispatch({type:"CUSTOMERS_FETCH_SUCCESS", customers: res.json()}) then.
  • Rishabh Nigam
    Rishabh Nigam almost 4 years
    This writeup is really helpful