In the chapter on Designing the State Shape, the docs suggest to keep your state in an object keyed by ID:
Keep every entity in an object stored with an ID as a key, and use IDs to reference it from other entities, or lists.
They go on to state
Think of the app’s state as a database.
I'm working on the state shape for a list of filters, some of which will be open (they're displayed in a popup), or have selected options. When I read "Think of the app’s state as a database," I thought about thinking of them as a JSON response as it would be returned from an API (itself backed by a database).
So I was thinking of it as
[{
id: '1',
name: 'View',
open: false,
options: ['10', '11', '12', '13'],
selectedOption: ['10'],
parent: null,
},
{
id: '10',
name: 'Time & Fees',
open: false,
options: ['20', '21', '22', '23', '24'],
selectedOption: null,
parent: '1',
}]
However, the docs suggest a format more like
{
1: {
name: 'View',
open: false,
options: ['10', '11', '12', '13'],
selectedOption: ['10'],
parent: null,
},
10: {
name: 'Time & Fees',
open: false,
options: ['20', '21', '22', '23', '24'],
selectedOption: null,
parent: '1',
}
}
In theory, it shouldn't matter as long as the data is serializable (under the heading "State").
So I went with the array-of-objects approach happily, until I was writing my reducer.
With the object-keyed-by-id approach (and liberal use of the spread syntax), the OPEN_FILTER
part of the reducer becomes
switch (action.type) {
case OPEN_FILTER: {
return { ...state, { ...state[action.id], open: true } }
}
Whereas with the array-of-objects approach, it's the more verbose (and helper function reliant)
switch (action.type) {
case OPEN_FILTER: {
// relies on getFilterById helper function
const filter = getFilterById(state, action.id);
const index = state.indexOf(filter);
return state
.slice(0, index)
.concat([{ ...filter, open: true }])
.concat(state.slice(index + 1));
}
...
1) Is the simplicity of the reducer the motivation for going with the object-keyed-by-id approach? Are there other advantages to that state shape?
and
2) It seems like the object-keyed-by-id approach makes it harder to deal with standard JSON in/out for an API. (That's why I went with the array of objects in the first place.) So if you go with that approach, do you just use a function to transform it back and forth between JSON format and state shape format? That seems clunky. (Though if you advocate that approach, is part of your reasoning that that's less clunky than the array-of-objects reducer above?)
and
3) I know Dan Abramov designed redux to theoretically be state-data-structure agnostic (as suggested by "By convention, the top-level state is an object or some other key-value collection like a Map, but technically it can be any type," emphasis mine). But given the above, is it just "recommended" to keep it an object keyed by ID, or are there other unforeseen pain points I'm going to run into by using an array of objects that make it such that I should just abort that plan and try to stick with an object keyed by ID?
Q1: The simplicity of the reducer is a result of not having to search through the array to find the right entry. Not having to search through the array is the advantage. Selectors and other data accessors may and often do access these items by id
. Having to search through the array for each access becomes a performance issue. When your arrays get larger, the performance issue worsens steeply. Also, as your app becomes more complex, showing and filtering data in more places, the issue worsens as well. The combination can be detrimental. By accessing the items by id
, the access time changes from O(n)
to O(1)
, which for large n
(here array items) makes a huge difference.
Q2: You can use normalizr
to help you with the conversion from API to store. As of normalizr V3.1.0 you can use denormalize to go the other way. That said, Apps are often more consumers than producers of data and as such the conversion to store is usually done more frequently.
Q3: The issues you'll run into by using an array are not so much issues with the storage convention and/or incompatibilities, but more performance issues.