Redux Entity Classes: An Alternative to Selectors

Tuesday, May 30, 2017

By Traction

Redux is an awesome tool to handle the state of your apps.

Used correctly, it can help you add features to your development process that can save you many headaches. However, Redux by itself won't solve all of your problems. You still have to build your app, design the state structure, dispatch actions, handle state updates, query data from the state, etc.

One of the most interesting parts is querying data from the state. I'd like to be able to get my data and use it everywhere, regardless of whether I'm using a framework like React, Vue.js or just plain vanilla JS. The suggested way to handle these queries is using selectors that take the current state as the first parameter and use it to reduce or filter data.

 
function
 getBrandById(state, id){ 
// return the Brand by id from the state
 
return
 state.brands.find( e => { 
return
 e.id === id; }) } 

In this example, we are using a simple function to query the required data: it will find the Brand and return it as an object.

Selectors work as independent, predictable, and testable functions that can be used within other selectors to create more complex queries. So, if you want to get the Brand of a Device you can nest functions to get the required data.

 
function
 getDeviceBrand(state, device){ 
// return the Brand of the device
 
return
 getBrandById(state, device.brandId); } 

An alternative approach using entities

In this exercise, we will use the following data as our state:

{"
brands
":[ {"name":"
Lenovo
","id":
1
}, {"name":"
Apple
","id":
2
} ], "
devices
":[ {"id":1,"name":"
Tablet
","brand":
2
}, {"id":2,"name":"
PC
","brand":
1
}, {"id":3,"name":"
Laptop
","brand":
1
}, {"id":4,"name":"
Laptop
","brand":
2
}] } 

Following the selectors idea, we can use a more object-oriented approach using the ES6 class syntax and static functions.

The basic concept is to create entity classes with static functions in them that handle queries related to that specific entity. This lets us create entities to suit our query needs and prepare data that's easy for an app to display and consume.

 
Class
 Brand { 
static
 getById (state, id) 
// returns the Brand instance corresponding to that ID.
 } 

So, what's the difference?

First, we're not passing in the state of the app as a parameter, which I talk about later on. Second, these are static functions inside a class. And the third and most useful difference is that instead of only returning a simple object or array of objects, these functions return an instance or array of instances of that class when possible. This allows us to design our entity to contain a lot of functionality.

var
 brand = Brand.getById(state, 12345 ) 
// gets the Brand instance of that Id.
 
var
 allBrands = Brand.getAll() 
// gets an array of Brand objects with of all the available Brands in the state. 
 brand.anyProperty 
//get a property populated by the data in the state.
 brand.anyFunction(); 
//call a function that uses the specific data of that instance.
 Device.getById(state, 123 ).brand; 
// get a device instance by id, then get the brand instance associated to that device.
 

Returning an instance

When your static function returns an instance of a class, you get the advantages that a class provides. For example, you can use a constructor to build an object, have getters and setters for specific properties, and even incorporate complex functions.

The following is an example of how the Device class could look with its respective constructor:

class
 Brand { 
constructor
 (
state
, 
params
) { this.id = params.id; this.name = params.name; } 
 // make use of the setter to make some transformation to the provided data.
 
set
 name (
name
) { 
if
 (name) { this._name = name.toUpperCase(); } } 
 // returns the Brand instance corresponding to that ID.
 
static
 getById (
state
, 
id
) { 
let
 obj = state.brands.find( e => { 
return
 e.id === id; }); 
return
 
new
 Brand(state, obj); } } 

In this example, the first thing we do is define the Brand class. The constructor for this class takes an object with the Brand parameters from our state, which can then be transformed or assigned to the instance. Next, a setter transforms data and assigns it to the class instance. Finally, a static function takes in the state and the ID of the Brand, queries data from the state and returns a new instance of the same Class.

Data association

Data association is useful when you work on relational entities that have many links between them. Because we can set whatever we want inside the constructor, we can associate necessary data at the same time. Let's make a class called Device, where each device has an associated brand in the brandId property:

 
//get the brand name of the device by id.
 
var
 device = Device.getById(state, 123).brand.name 

To do this, we need to associate the brand with the device on class instantiation.

class
 Device { 
constructor
 (
state
, 
params
) { this.id = params.id; this.name = params.name; this.brand = Brand.getById(state, params.brand); } 
static
 getById (
state
, 
id
) { 
let
 obj = state.devices.find( e =>{ 
return
 e.id === id; }); 
return
 
new
 Device(state, obj); } } 

Updated state for everyone

Depending on your app dev environment and configuration, there are many ways to make the updated state for your classes available to your app. The recommended way is to subscribe to the store and get the new state every time an action is triggered, then pass in the state and make it available for all the JS that needs it. But for this exercise, we'll pretend we are working on a modular app and make the state available in a Class that has a static variable that always updates using a middleware function.

Think of this middleware as the bridge between the triggered action and the data storage: We can intercept this process and, before storing the data, make this new store data available in a variable. The data can now be used everywhere and allows our class to be a bit cleaner while still dependant on the app state.

//simple middleware that updates the state into a static variable
 const stateUpdater = store => next => action => { 
let
 nextState = next(action); AppState.state = store.getState(); 
return
 nextState; } 
class
 Device { 
constructor
 (
params
) { this.id = params.id; this.name = params.name; this.brand = Brand.getById(params.brand); } 
static
 getById (
id
) { 
let
 obj = stateStatic.devices.find( e =>{ 
return
 e.id === id; }); 
return
 
new
 Device(state, obj); } } 

JSBin example — with the updated state as a variable.

JSBIN example 2 — with the updated state available in a class with a static variable

Conclusion

This approach allows us to create entities in a way that suits our query needs. It's very useful to have an entity that represents our data — and prepares it in a way that our app can easily display and utilize. Imagine the possibilities of using this approach in a larger application. Entities can be linked to each other and provide the relational data in one Object, which then has the ability to create more complex queries just as you would with selectors.

Your imagination is the only limitation.

Recent articles
Thursday, May 25, 2017Giving Employees Paid Leave for Activism Ignites Firestorm

Some people call me a "candy ass." And I'm okay with that.

Thursday, May 25, 2017What's the Future of Graphic Design?

The future of graphic design is immersion.

Sunday, May 21, 2017Re: Story, Thinking, & "Oh Shit" Questions

Jenn Maer of IDEO talks about her background as a story/copywriter, IDEO's "design thinking" approach, and the big "oh shit" questions that they strive to answer.

Traction is a new breed of marketing agency and consultancy—a marketing accelerator for brands and their in-house teams.

© 2000 – 2024

Post

5214F Diamond Hts Blvd #1052 San Francisco, CA 94131 415.962.5800

New business

Careers

General

Privacy Policy