Redux - Normalized nested data organization inside createSlice

2.5k views Asked by At

I have a deeply nested data object that comes back from my API which looks like the JSON below.

I am using Redux toolkit's createSlice to create a slice of a trip

So currently in my createSlice, I want to store an array of trips.

I also want the ability to update a single trip or part of the trip

  • for example, let's say I want to update a trip item's start date
  • or, let's say I want to update a trip item's member's name

My questions and concerns:

  • I currently have all of these entities coming back into the trip createSlice but I am not sure if, once the entities are normalized, should they be separated into separate createSlices? if so, how is this done or is this an anti pattern?
  • how should nested entities be defined in initialState?
  • should I define all of my normalized entities in my initalState? if I do that, how would my reducers look like when I want to update a trip_item or trip_item_member ?
  • does my normalized data even look "correct"? I have omitted using mergeStrategy between trips_items_members and trip_members which I know I should do but haven't figured out how that works yet or if it's necessary here?

Note:

There is an example in the RTK docs here which shows createSlice being used with 3 separate entities, which originally came from 1 API call. It looks like 3 separate files however it is unclear how data is shared amongst them.

This is how my trip createSlice looks like

/**
 * Get trip by ID action
 */
export const getTripByID = createAsyncThunk(
  'trips/getTripByID',
  async ({ uid }) => {
    const response = await findOne(uid)
    const normalized = normalize(response, trip)
    return normalized.entities
  },
)

const tripsAdapter = createEntityAdapter({
  selectId: entity => entity.trip_id,
  sortComparer: (a, b) => b.start_date.localeCompare(a.start_date),
  loading: '',
  error: '',
  data: [],
})

export const {
  selectById: selectTripById,
  selectIds: selectTripIds,
  selectEntities: selectTripEntities,
  selectAll: selectAllTrips,
  selectTotal: selectTotalTrips,
} = tripsAdapter.getSelectors(state => state.trip)


const initialState = tripsAdapter.getInitialState()

const tripSlice = createSlice({
  name: 'trips',
  initialState,
  extraReducers: builder => {
    builder.addCase(getAllTrips.fulfilled, (state, { payload }) => {
      tripsAdapter.upsertMany(state, payload)
      state.loading = false
    })
    builder.addCase(getTripByID.fulfilled, (state, { payload }) => {
      console.log('payload', payload)
      tripsAdapter.upsertMany(state, payload)
      state.loading = false
    })
  },
})

export default tripSlice.reducer

API response that comes back from await findOne(uid)

{
    created_by: "6040c2d1-ea57-43b6-b5f2-58e84b220f4e",
    deleted_by: null,
    destination: "Valencia",
    end_date: "2020-10-04",
    start_date: "2020-09-27",
    trip_id: "34a620e8-51ff-4572-b466-a950a8ce1c8a",
    uid: "14047a5b-2fe5-46c9-b7f2-e9b5d14db05b",
    updated_by: null,
    trip_items: [
        {
            destination: "Mezzanine Level Shivaji Stadium Metro Station, Baba Kharak Singh Rd, Hanuman Road Area, Connaught Place, New Delhi, Delhi 110001, India",
            end_date: "2020-09-28",
            end_time: "2020-09-28T01:20:15.906Z",
            note: null,
            start_date: "2020-09-28",
            start_time: "2020-09-28T01:20:15.906Z",
            trip_item_id: "bd775be7-2129-42c0-a231-5a568b0f565d",
            trips_items_members: [
                {
                    trip_item_member_id: "76b54a80-4d09-4768-bc5a-4d7e153e66dc", 
                    uid: "4b88f9af-8639-4bb0-93fa-96fe97e03d02", 
                }
            ],
            uid: "e5f81a6d-1a0d-4456-9d4e-579e80bc27d8",
        }
    ],
    trips_members: [
        {
            trip_member_id: "76b54a80-4d09-4768-bc5a-4d7e153e66dc", 
            uid: "4b88f9af-8639-4bb0-93fa-96fe97e03d02", 
            role: "ADMIN"
        }
    ]
}

This is my normalizr schema


const tripItemMember = new schema.Entity(
  'trips_items_members',
  {},
  { idAttribute: 'trip_item_member_id' },
)

const tripItem = new schema.Entity(
  'trips_items',
  {
    trips_items_members: [tripItemMember],
  },
  {
    idAttribute: 'trip_item_id',
  },
)

const tripMember = new schema.Entity(
  'trips_members',
  {},
  {
    idAttribute: 'trip_member_id',
  },
)

export const trip = new schema.Entity(
  'trip',
  {
    trips_items: [tripItem],
    trips_members: [tripMember],
  },
  {
    idAttribute: 'trip_id',
  },
)

This is the output from normalizr

trip: {
  "34a620e8-51ff-4572-b466-a950a8ce1c8a": {
    created_by: "6040c2d1-ea57-43b6-b5f2-58e84b220f4e"
    deleted_by: null
    destination: "Valencia"
    end_date: "2020-10-04"
    start_date: "2020-09-27"
    trip_id: "34a620e8-51ff-4572-b466-a950a8ce1c8a"
    trips_items: ["bd775be7-2129-42c0-a231-5a568b0f565d"]
    trips_members: ["76b54a80-4d09-4768-bc5a-4d7e153e66dc"]
    uid: "14047a5b-2fe5-46c9-b7f2-e9b5d14db05b"
    updated_by: null
  }
}

trips_items:{
  "0a56da0f-f13b-4c3d-896d-30bccbe48a5a": {
    destination: "Mezzanine Level Shivaji Stadium Metro Station"
    end_date: "2020-09-28"
    end_time: "2020-09-28T01:20:15.906Z"
    note: null
    start_date: "2020-09-28"
    start_time: "2020-09-28T01:20:15.906Z"
    trip_item_id: "0a56da0f-f13b-4c3d-896d-30bccbe48a5a"
    trips_items_members: []
    uid: "25d20a9d-1eb9-4226-926d-4d743aa9d5dc"
  }
}

trips_members: {
  "76b54a80-4d09-4768-bc5a-4d7e153e66dc": {
    role: "ADMIN"
    trip_member_id: "76b54a80-4d09-4768-bc5a-4d7e153e66dc"
    uid: "4b88f9af-8639-4bb0-93fa-96fe97e03d02"
  }
}
1

There are 1 answers

0
Linda Paiste On

Your setup is very much like this detailed example from the redux-toolkit docs. They are fetching articles, but each article comes with embedded users and comments. They define separate slices for each of the three entities.

The comments slice has no actions or reducers of its own, but it uses the extraReducers property to respond to the article received action and store the embedded comments.

const commentsAdapter = createEntityAdapter();

export const slice = createSlice({
  name: "comments",
  initialState: commentsAdapter.getInitialState(),
  reducers: {},
  extraReducers: {
    [fetchArticle.fulfilled]: (state, action) => {
      commentsAdapter.upsertMany(state, action.payload.comments);
    }
  }
});

The fetchArticle action is "owned" by the article slice, but the action payload contains entities from all three types. All slices receive all actions, so the comments and users are able to respond to this action with their own logic. Each slice doesn't have any effect on what the others can or can't do.

In your case you want to create slices for items and members. Instead of calling upsertMany(state, payload), you want the payload to be keyed by entity type so that you can call upsertMany(state, payload.members).