Redux Entity Classes: an alternative to selectors

Redux is an awesome tool for handling the state of your app. 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.

Jaime García III Senior Front-End Developer

Jaime is a versatile creative Engineer that enjoys working in all kinds of projects like Full stack web development, Email Development, Video Game development, interactive apps, Animation, UX, and even a bit of Design.