In our reducer, we’re updating a nested array with new data coming from the backend.
However, certain UI-specific properties (like tooltip
) that are not part of the backend response need to be preserved from the existing state.
For example, let’s say we have the following structure:
state.items = [
{
id: "1",
details: [
{
id: "a",
value: "foo",
uiState: { tooltip: "original" },
},
],
},
];
And the backend returns an update like this:
update = [
{
id: "1",
details: [
{
id: "a",
value: "bar", // ✅ updated
// ❌ tooltip is missing
},
],
},
];
To preserve the tooltip, we currently merge it manually like this:
details: oldItem.details.map((d, i) => ({
...d,
...newItem.details[i],
uiState: d.uiState, // preserve UI state
}))
This works, but feels brittle and hard to maintain as more fields or nested structures are added.
Does the Redux Store provide a recommended utility or pattern to handle this kind of merging where some fields should be preserved?
Or is this type of field-level merge expected to be handled manually within each reducer?
If there's an existing example or best practice around this, I'd really appreciate any pointers.
details: oldItem.details.map((d, i) => ({ ...d, ...newItem.details[i], uiState: d.uiState, // preserve UI state }))
This works, but feels brittle and hard to maintain as more fields or nested structures are added.
I don't think I'd consider manually merging existing state into new state updates as brittle or difficult to maintain. It used to be the default standard way to update state. See the Immutable Update Pattern that required users to manually shallow copy all current state they are updating.
Does the Redux Store provide a recommended utility or pattern to handle this kind of merging where some fields should be preserved?
Recommended Utility: No. Redux is rather un-opinionated with regards to how you maintain/update your states. They have few hard rules:
Legacy Redux: Apply Immutable Update Patterns
Modern Redux: Apply Mutable State Updates
state
or return an entirely new state object/reference, and never both.state
can never be reassigned.Recommended Pattern: Yes, see the Modern Redux point above.
Or is this type of field-level merge expected to be handled manually within each reducer?
Basically, yes. If there is some state you need to persist from the previous state you will generally need to manually handle this.
Examples:
Map the current state and merge the new details in:
details: oldItem.details.map(
({ uiState, /* other properties to keep, */ ...overwritableDetails }, i) => ({
...overwritableDetails,
...newItem.details[i],
... { uiState, /* other properties to keep, */ }
})
)
Map the new details array and merge in old details:
details: newItem.details.map((detail, i) => {
const { uiState, /* other properties to keep, */ } = details[i];
return {
...detail,
... { uiState, /* other properties to keep, */ }
}
})
While the only basic tool Redux offers for state updates is implementing Immer under-the-hood which allows for writing mutable state updates, you can reach out and use other 3rd-party tools to manage merging objects. For example, you could use Lodash's merge utility to merge the new details into the existing details.
This method is like
_.assign
except that it recursively merges own and inherited enumerable string keyed properties of source objects into the destination object. Source properties that resolve to undefined are skipped if a destination value exists. Array and plain object properties are merged recursively. Other objects and value types are overridden by assignment. Source objects are applied from left to right. Subsequent sources overwrite property assignments of previous sources.
An example implementation might look like:
import { merge } from "lodash";
details: oldItem.details.map(
(detail, i) => merge(detail, newItem.details[i]) // returns mutated detail
)
or
details: oldItem.details.forEach((detail, i) => {
merge(detail, newItem.details[i]); // mutates detail
})
The oldItem.details[i].uiState
and any other properties that don't exist in newItems.details
should not be overwritten and should be kept in the updated state result.