Normalizing Relational Data into Redux State with Normalizr

Retrospective edit: While normalization has proven to be an effective technique for state management, the abstraction introduced by tools like Normalizr can sometimes be a burden over manual normalization. As always, it’s important to consider the tradeoffs of your dependencies.

Normalization is a popular strategy for intelligent redux state management for large applications. This strategy is even recommended in the Redux documentation itself.

“The recommended approach to managing relational or nested data in a Redux store is to treat a portion of your store as if it were a database, and keep that data in a normalized form.” ~ Redux

What is Data Normalization?

Normalization is the process of organizing data so that it has logical relationships and no redundancy.

Normalized data has several advantages over nested denormalized data:

  • Improved performance
  • Small state footprint
  • No redundant data to keep in sync
  • Simplified immutability patterns (due to less nesting)

When Would I Use Normalization?

You’ll often use normalization to translate redundant nested data retrieved from an API into simple manageable state.

The gist is that many apis will return data in a nested denormalized structure like the following:

parents: [{
id: 3,
type: 'parent',
name: "Bob",
schools: [{
id: 1,
type: 'school',
name: "Morse High School",
district: {
id: 1,
type: 'district',
name: "SDUSD"
}
}, ...]
}, ...]

Normalized, this data might look like the following:

districts: {
1: {
id: 1,
type: 'district',
name: "SDUSD",
schools: [1, 2, 3, ...],
parents: [1, 2, 3, 4, 5, ...]
}, ...
},
schools: {
1: {
id: 1,
type: 'school',
name: "Morse High School,
district: 1,
parents: [3, 4, 5, ...]
}, ...
},
parents: {
3: {
id: 3,
type: 'parent',
name: "Bob",
districts: [1, ...],
schools: [1, ...]
}, ...
}

Normalizr

There is a decent amount of reference material on using Normalizr to normalize very simple denormalized data into very simple normalized data (See the Normalizr docs for some examples). However, there isn’t much material on transforming complex nested relational data into simpler normalized relational data. Below is some guidance on how you might go about such a task

In our example above, you may have noticed the following relationships:

  • one parent to many districts
  • many parents to many schools
  • many schools to one district

Below is the code Normalizr’s examples start you with to achieve the desired transformations:

import { schema } from 'normalizr'const districtSchema = new schema.Entity('districts', {})
const schoolSchema = new schema.Entity('schools', {})
const parentSchema = new schema.Entity('parents', {})
// A parent has one district and many schools
parentSchema.define({
district: districtSchema,
schools: [schoolSchema]
})
// A school has one district and many parents
schoolSchema.define({
district: districtSchema,
parents: [parentSchema]
})
// A district has many schools and many parents
districtSchema.define({
schools: [schoolSchema],
parents: [parentSchema]
})
// We received our parent data as an array, so we need a separate
// schema to represent that list of parents
export const parentListSchema = [parentSchema]
normalize(DENORMALIZED_PARENTS_DATA, parentListSchema)

This gets us a third of the way to our goal. By default, we will be returned an array of schools, an array of districts, and an array of parents. As desired, our parents will have an array of school ids and a district id attached to it for use in referencing the others. However, schools will have no lists of parent ids, and districts will have neither a list of school ids nor parent ids.

To enable these reciprocal relationships, we’ll need to alter the way Normalizr processes our data. There are two functions we’ll be overriding to do this.

  • processStrategy — tells Normalizr to add, remove, or change data in the instance during a pre-processing step, given access to the instance (e.g. district object), its parent instance (e.g. school object), and the key we’re currently processing (e.g. ‘districts’).
  • mergeStrategy — tells Normalizr how to combine an entity instance with another entity instance with the same id value.
// In our denormalized data, 'parent' is the parent object to school
// In an api call to districts, district might be the parent.
// As such, we define our behavior for the case of 'parent'.
// In this case, we add a 'parents' property to the school and
// set it equal to an array with just this parent id inside. When
// the parent object is a district, we just return the school as is.
const schoolProcessStrategy = (value, parentObj) => {
switch (parentObj.type) {
case 'parent': {
return { ...value, parents: [parentObj.id] }
}
default:
return value
}
// With the default merge strategy, each school would have an
// array of only the parent id of the final parent processed,
// because we would overwrite the 'parents' property each time we
// create a 'parent' (mergeStrategy defaults to use the newest data)
// This new merge strategy will merge the school's existing
// 'parents' array with that of the return value from the school
// process strategy.
const schoolMergeStrategy = (objA, objB) => ({
...objA,
...objB,
parents: [...(objA.parents || []), ...(objB.parents || [])]
})
// Anytime we are processing a district, give the district an
// additional property 'parents', and set it equal to an array
// including the contents of parentObj.parents ('school' is
// parentObj in our denormalized example).
const districtProcessStrategy = (value, parentObj) => {
return { ...value, parents: [...(parentObj.parents || [])] }
}
// With the default merge strategy, each district would have only
// the parents of the final school processed, because we would
// overwrite the parents property each time we create a district.
// This new merge strategy will merge the district's existing
// parents array with that of the return value from the district
// process strategy.
const districtMergeStrategy = (objA, objB) => ({
...objA,
...objB,
parent: [...(objA.parents || []), ...(objB.parents || [])]
})

Next, we’ll need to add these strategies to the respective schema definitions to tie everything together.

const districtSchema = new schema.Entity(
'districts',
{},
{
processStrategy: districtProcessStrategy,
mergeStrategy: districtMergeStrategy
}
)
const schoolSchema = new schema.Entity(
'schools',
{},
{
processStrategy: schoolProcessStrategy,
mergeStrategy: schoolMergeStrategy
}
)

You should now have the normalized data with reciprocal id references we were after.

Postface: Please use this code only for illustrative purposes regarding creating complex relationships with normalizr. Before it’s ready for use, you’ll want to test it and remedy some issues. For example, you’ll receive duplicate ids in your reference arrays because of our blind concat merging. This is simple to do (e.g. _.uniq()), but beyond the scope of the example.

I hope this helps. If you have any questions, corrections, or improvements, I would love the opportunity to learn from it!

Cheers,
Justin

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Justin Ross

Justin Ross

Software Engineer and Dog Evangelist.