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 separatecreateSlice
s? 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 atrip_item
ortrip_item_member
? - does my normalized data even look "correct"? I have omitted using
mergeStrategy
betweentrips_items_members
andtrip_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"
}
}
Your setup is very much like this detailed example from the redux-toolkit docs. They are fetching
articles
, but each article comes with embeddedusers
andcomments
. They define separate slices for each of the three entities.The
comments
slice has no actions or reducers of its own, but it uses theextraReducers
property to respond to the article received action and store the embedded comments.The
fetchArticle
action is "owned" by thearticle
slice, but the action payload contains entities from all three types. All slices receive all actions, so thecomments
andusers
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
andmembers
. Instead of callingupsertMany(state, payload)
, you want the payload to be keyed by entity type so that you can callupsertMany(state, payload.members)
.