Dispatch reducers from another slice in Redux/Redux Toolkit

Viewed 928

The Setup

In my backend, I have a Musician model which can have many Instruments or Songs. The rest API has separate routes to get all Musicians, get all the Instruments for a musician, get all the Songs for a musician, etc.

My root state looks like this:

musicians: [],
instruments: [],
songs: []

I then have 3 Redux Toolkit "slices" that manage those respective parts of state. For example, instrumentsSlice has an async thunk called fetchInstruments that calls the rest API to fetch instruments for a specific musician, then is fulfilled in extraReducers:

extraReducers: {
[fetchInstruments.fulfilled]: (state, action) => {
   return action.payload.map(instrument => {
      instrument.id = nextInstrumentId++;
      return instrument;
  });
},
  ....
}

On the page for a specific musician: fetchInstruments and fetchSongs are dispatched, which call their respective APIs that update the state. The Selectors I use then filter only the Instruments or Songs that match the particular Musician ID for that page.

The Problem

Up to this point, I've been able to get away with using these 3 separate slices. Now, I want to create a dashboard where I can view summary information for all Musicians. I realized that it wouldn't make sense to call 3 XHR requests per every Musician listed, so I created a single new rest route that returns the information nested for all musicians, fetchAllMusicianData, i.e.:

musicians: [
 {
   name:,
   instruments: [],
   songs: []
 },
 ...
 ]

The Question

Somehow, I need to get this nested Musician/Instrument/Song data into my state tree. However, ideally, I don't want to duplicate logic that exists in the fetchInstruments or fetchSongs reducers (such as creating local ids in my extraReducers code above).

Is there some way that when the fetchAllMusicianData call is fulfilled, I can call each of those other slices to run their methods like fetchInstruments.fulfilled? Is there some way I need to abstract updating this information? Or, should I be organizing my state in a different way?

Thanks for the help in advance!

1 Answers

Let's get one thing out of the way first. You should not increase your ids like this in general:

extraReducers: {
[fetchInstruments.fulfilled]: (state, action) => {
   return action.payload.map(instrument => {
      instrument.id = nextInstrumentId++;
      return instrument;
  });
},
  ....
}

As nextInstrumentId is an external variable, you are introducing a side effect here - so if you were to replay your actions using the devtools (stuff like for examples "skipping" an action will replay them from the start), you would get different ids a second time. And if you had other actions that referenced those ids (or local state or a url parameter etc.), that reference would break.

So, those ids would either need to be generated based only on information available in the reducer or even better, directly in your createAsyncThunk call. Doing such a side effect there is perfectly fine.

Now, back to the question at hand:

My suggestion would be to change the return value of your fetchInstruments so that it shares the same (partial) structure as your fetchAllMusicianData asyncThunk.

So,

const fetchInstruments = createAsyncThunk('fetchInstruments', () => {
  // get the data:
  const fetched = fetchData();
  // apply some ids
  const instruments = fetched.map(instrument => ({ ...instrument, id: nextInstrumentId++ }))
  // return it as part of an object
  return { instruments }
}

Then do the same for your fetchAllMusicianData:

const fetchAllMusicianData = createAsyncThunk('fetchAllMusicianData', () => {
  // get the data apply some ids, you get the gist

  // return it as an object
  return { instruments, songs, name }
}

And now you can have all slice just handle fetchInstruments and fetchAllMusicianData (or fetchSongs and fetchAllMusicianData etc.) the same.

Use the extraReducers builder notation with a matcher for that:

extraReducers(builder) {
  builder
    .addMatcher(
      isFulfilled(fetchInstruments, fetchAllMusicianData),
      (state, action) => {
        // or whatever other logic you need here
        return action.payload.instruments;
      }
    )
}

and that should do it in the end.

Related