Working with normalized state in Redux applications

In pre-Redux, pre-immutable front-end world there was mostly no need to have a normalized application state. Building your application state structure was simple – you would just use multiple models which refer each other. Then one model instance can be easily referred by multiple other models and each model can be updated individually without affecting the rest of the application state.

In Redux world, sooner or later you will end up with a need to normalize your state (note: this post will not explain why you would normalize the Redux state in a large app – this deserves a separate post). Accessing entities in a normalized state might be a complicated task because data related to one entity is scattered across multiple state slices. So naturally, you will start exploring ways to simplify the reasoning about application state. This article proposes a way to solve this problem.

How does the normalized state look? Let’s say, your application manages records of drivers and their cars. In pre-Redux world, you would ‘nest’ objects by references, so there would be an ‘intuitive’ and ‘straightforward’ way to get cars of a driver (someDriver.cars). Here is how normalized state for this app might look like:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

{

drivers:{

'1':{

id:'1',

name:'Kevin Potter'

},

'2':{

id:'2',

name:'Keith Johnson'

}

},

cars:{

'3':{

id:'3',

name:'Daewoo Lanos',

driverId:'1'

},

'4':{

id:'4',

name:'McLaren',

driverId:'2'

},

'5':{

id:'5',

name:'Ferrari',

driverId:'2'

}

}

}

– please note that I deliberately chose to have Car record referring its Driver and not to have Driver referring his list of car IDs. More on this choice later as this is not the main goal of this post

As you see from example above, normalized state is very different – you can’t access cars of a certain driver by just drilling down the properties of the object, so you can’t do something like driver.cars. Instead, you will need to get cars who’s driverId matches id of your driver. Here’s how this would look like:

1

2

3

4

constdriverId='2';

constcarsOfDriver=Object.keys(state.cars)

.map((carId:string)=>state.cars[carId])

.filter((car:Car)=>car.driverId===driverId);

– please note that all examples are written in TypeScript because I hardly see writing a large-scale application in pure JS

As you see that’s a lot of code to just get cars of the driver, compare this with someDriver.cars in non-normalized state.

Now imagine that you will need this functionality in a lot of places – anywhere where you need to access cars of a driver. It is, of course, reasonable to extract this code into a separate function (redux selector for example), let’s say you put it in DriverSelector selector. But as the application grows and amount of normalized state slices grows, you will find that you start forgetting which selector to use in which case. Compare this selectors approach with non-Redux application model management – there you would not even need to remember how to get cars of the driver, intellisense is always there to help you to write someDriver.cars.

But in our normalized Redux state there’s nothing that tells you what selector to use in which case, as there’s no link between your state shape and selectors that you need to use. And the biggest problem of the normalized state is that it does not represent a human-readable world which we are used to. Our brains are not trained to think in normalized forms.

Introducing ‘Projection’

In Redux application, you wish to just think about Driver entity and not think how it is stored in the state, that parts of entity are stored in different state slice (Cars). Let’s think which API will be suitable to achieve this simplification. Ideally, we are trying to get back to someDriver.cars simplicity from non-normalized state world. Here’s what we would be happy with:

1

constcarsOfDriver=driverApi.cars;

If we get driverApi to indeed represent Driver entity and hide the normalization of the state for us, we should be happy.

Here’s where the pattern Projection comes in. It takes the idea from the ORM principles (normalize Redux state is very similar to a relational database). The idea is to create a View (or Projection) for your entity and let this view ‘gather’ data from multiple state slices. Then, when you need to access your state, you no longer access it via state.someProperty, you don’t access the state directly at all. Instead, you always use Projection which transforms normalized, non-readable state into a shape which human brain can easily work with. Here’s how the use of such projection API will look like:

1

2

constdriver=newDriverProjection(state,'2');

constcarsOfDriver=driver.cars;

And here’s an implementation of this Projection pattern:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

// private function inside your Projection module

functiongetCarsForDriver(state:AppState,driverId:string):Array<Car>{

returnObject.keys(state.cars)

.map((id:string)=>state.cars[id])

.filter((car:Car)=>car.driverId===driverId);

}

classDriverProjection{

constructor(privatestate:AppState,privatedriverId:string){

}

get cars():Array<Car>{

returngetCarsForDriver(this.state,driverId);

}

}

As you see, the structure of the state is encapsulated behind the getter, so developer who uses projection does not need to know the shape of the complex normalized state to access the data. It is important to understand Projection’s main benefit compared with selector (because one might say that selector hides the state structure as well). The difference is that you can hide multiple getters and selectors under one projection and let users of your projection API only think about high-level application entity – Driver and intellisense will guide the user through possible properties of the entity. So we are going back to a non-normalized, ‘straightforward’ and natural reasoning about application state. This is hardly achievable when having multiple selectors scattered all over your app. Projectiongroups selectors for you and hides them under the facade that is easily understood, you can say that Projection ‘de-normalizes’ the state for you.

Memoization

There is one optimization that we can make to Projection. As you see, each invocation of cars getter will create a new array. We can prevent this with memoization. If state and driverId don’t change, we should not recalculate the array. You are welcome to use any memoization library, here we will stop on reselect as this is what a lot of people in React-Redux community are used to:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

import{createSelector}from'reselect';

// private function inside your Projection module

constgetCarsForDriver=(cars:Cars,driverId:string):Array<Car>=>

Object.keys(cars)

.map((id:string)=>cars[id])

.filter((car:Car)=>car.driverId===driverId);

classDriverProjection{

// private instance of memoized selector

privatecarsForDriverSelector=createSelector(

state=>state.cars,

(state,driverId)=>driverId,

getCarsForDriver

);

constructor(privatestate:AppState,privatedriverId:string){

}

get cars():Array<Car>{

returnthis.carsForDriverSelector(this.state,this.driverId);

}

}

Now every .cars getter invocation will return the same instance of array.

Using in React

The great thing about ‘Projection’ approach is that you can also use it in connected React-Redux components. Here’s your mapStateToProps of the connected component which displays cars of a given driver:

As you see, we again don’t need to use selectors at all – we only use Driver projection.

There’s a final optimization that we can do here – making sure that our projection instance is not recreated on every store update ( our memoized selector would not actually work if new projection instance is created every time). For this optimization let’s add a method update to the projection which allows us to update the state value and not recreate selectors every time:

Summary

One might say that such Projection approach over-complicates the application. It is indeed true for most Redux applications, specifically for those which do not use normalized state.

But as soon as you try to write a large, scalable React-Redux application, you will face with a problem of reasoning about the normalized state. Unfortunately, sometimes you might realize the scale of the problem when it’s too late to go back and fix things. So please consider this or similar approach if you are writing a large React-Redux application.