0

Let's say I am making simple card game in react using redux with redux-thunk middleware

I fetch the deck of cards from external API so my action creator looks like this:

const fetchDeck = {    
    return dispatch => {
        return fetch(https://some.api.with.cards/)
        .then(r => r.json())
        .then(data => dispatch({type:"FETCH_DECK", deck: data}))
    }
}

And the corresponding reducer is:

const deck = (state=[], action) => {
    switch(action.type) {
        case "FETCH_DECK":
            return action.deck

        default:
            return state
    }
}

The response from the API is an array of card objects like this one:

{
    name: "ACE",
    suit: "CLUBS"
}

So now I want to add hand prop to the store. hand is an array containing the cards which are already in the player's hand, so I created an action to allow to draw cards from deck to hand:

const drawCard = (deck, numberOfCards) => {
    return {
        type: "DRAW_CARD",
        deck
    }
}

And this is the corresponding reducer:

const hand = (state=[], action) {
    switch(action.type) {
        case "DRAW_CARD":
            return [...state.slice(0,action.deck)
        default:
            return state
    }
}

Finally I tried to implement this behaviour in the <Hand> component:

class Hand extends React.component {
    componentDidMount() {
        const { deck, drawCards } = this.props;
        drawCards(deck, 5)
    }
}

But it did not work! The API request is not completed when componentDidMount() occurs. The only thing came into my mind to handle the problem was:

componentDidUpdate(prevProps) {
    if (prevProps.deck.length > 0) {
        const { deck, drawCard } = this.props;
        drawCard(deck, 5)
    }
}

Well this works, but I have the awkward feeling that this is not the proper way to do it. Moreover it would be necessary to use some more condtitions to prevent unexpected behaviour if applied this way.

Is there a better way to handle my problem?

Przemek
  • 67
  • 1
  • 6

2 Answers2

2

I believe that the problem is in the approach. Instead of initializing deck in one place and hand in another place (componentDidMount) - you could create init action, which is called on the root component's componentDidMount.

It might look like this:

function init() {
  return (dispatch, getState) => {
     dispatch(fetchDeck()).then(() => dispatch(drawCard(getState().deck, 5)))
  }
}
Bsalex
  • 2,847
  • 1
  • 15
  • 22
  • Well this looks very promising, but according to what I have learned dispatching an action in another action creator is an anti-pattern which can lead to unexpected behaviour. Isn't that right? – Przemek Feb 08 '18 at 19:51
  • 1
    No, sorry, haven't heard about that. Isn't that what `redux-thunk` exists for? Please provide more info about the anti-pattern (link or something). – Bsalex Feb 08 '18 at 19:54
  • Actually, I was reffering to [this](https://stackoverflow.com/questions/36730793/can-i-dispatch-an-action-in-reducer) however it seems just reducer should not dispatching actions not action creators. Sorry I am just diving into redux, and feel miserable sometimes :). So to be clear: I do not any reducer corresponding to init() since it just dispatches other actions? – Przemek Feb 08 '18 at 20:03
0

First, you have to validate that deck is fetched before you use it, something like Array.isArray(deck) && drawCard() will be safer. Second, the right place for detecting that new props arrives is in componentWillReciveProps(), there you can invoke drawCard() safely, if deck was fetched.

Yossi
  • 445
  • 2
  • 9
  • Actually I tried to avoind ```componentWillReceiveProps()``` since it is going to be [depreciated](https://medium.com/@baphemot/whats-new-in-react-16-3-d2c9b7b6193b) soon – Przemek Feb 08 '18 at 19:55
  • It doesn't matter, because it will be replaced with static replacer method. The same will be achived in other way. – Yossi Feb 08 '18 at 19:57