Immutable.js wrapper utility
About me
Hi! I'm Rex Ogbemudia from Lagos Nigeria, I currently work at Andela as a Javascript software developer for the past one year. My focus has mainly been on the frontend, building our internal information management system using React/Redux.
What are we solving for?
When you are building large-scale applications using React/Redux and preventing state mutation seem to be a big issue, the first solution that may come to mind is using immutable.js collections. While this can be a nice idea, it also has its own drawbacks. One of its main drawback is the fact that for the view to consume data from the store, you either need to use the toJS
method, which is said to have its own performance issue or you have to introduce immutable.js APIs in your components. Adding immutable.js APIs to your react components make them tightly coupled to the immutable.js library, consequently preventing you from reusing your components without having to edit them.
Proxy to the rescue!
The primary responsibility of the view in your application is to display data. For React components to consume data from the store without adding immutable.js APIs in them, we need to create a wrapper around our immutable.js collections. To do this, we need to make use of the proxy API introduced to javascript in es6.
What are proxies? Proxies are objects in Javascript that wrap other objects. They have traps which are methods that intercept normal operations performed against a proxy, below is an example of a proxy.
var person = {
name: "john doe",
age: 12
}
var handler = {
get(target, key) {
console.log(`get person's ${key}`);
return target[key];
}
};
var personProxy = new Proxy(person, handler);
personProxy.name
// console.log => get person name
// return => john doe
Building an immuable.js object wrapper with proxy
Now I'm going to work us through how to build an immutable.js wrapper using a proxy;
Creating a handler object
A proxy handler is an object that holds different handler methods. Handlers methods are methods that intercept operations performed on an object. Visit this link to know more about proxy handlers.
In our case, we only need to make use of the get
handler method.
Handler class
class Handler {
get(target, name) {
//...
}
}
Above is our handler class and it has a get
method, the get
method accepts two arguments, target
and name
, target
is a reference to the wrapped object in our case an immutable.js collection and name
refers to the property we are trying to retrieve from our immutable collection.
class Handler {
get(target, name) {
const isValue = target.has(name);
if (isValue) {
const value = target.get(name);
return value
}
}
}
In the handler method we first check if the property is in the immutable.js collection, if yes, we call get()
on the immutable collection to retrieve the value and return it.
class Handler {
get(target, name) {
const isValue = target.has(name);
if (isValue) {
const value = target.get(name);
return value
}
const attribute = target[name];
if (typeof attribute === 'function') {
return (...args) => {
const value = attribute.apply(target, args);
return value;
};
}
return attribute;
}
}
If isValue
is false, we need to check if the caller is trying to access an immutable.js method or variable, examples of such methods and variables are get()
, set()
, has()
, size
e.t.c. To cater for this, we get the attribute, then check if the attribute is a function, if yes, we return a function else we return the attribute.
Creating a proxy function
Yeah! We have successfully created a proxy handler. We now have to create a proxy object, to do this we have to create a function.
function createImmutableProxy(value, throwError = false) {
if (!isImmutable(value)) {
if (throwError) throw new Error('Expected an immutable object');
return value;
}
const handler = new Handler();
return new Proxy(value, handler);
}
The function above creates an immutable proxy wrapper object when the value passed in is an immutable object, it throws an error or returns the value when it is not an immutable.js collection.
Nested collections
In the scenario where the value returned from our handler method is an immutable.js collection, we need to return a wrapped version of our immutable.js collection, to accomplish this, we need to make a few adjustments to the handler method.
class Handler {
get(target, name) {
const isValue = target.has(name);
if (isValue) {
const value = target.get(name);
return createImmutableProxy(value);
}
const attribute = target[name];
if (typeof attribute === 'function') {
return (...args) => {
const value = attribute.apply(target, args);
return createImmutableProxy(value);
};
}
return attribute;
}
}
Now, we have updated the handler method to return an immutable proxy wrapper when the value is an immutable.js collection else we return the value the way it is.
Final thoughts
It would be nice to have a trap for adding values to an immutable.js collection, but immutable.js collections always return a new object when we add to or update the object. A better way would be, for us to resist the temptation of modifying an immutable.js collection in the view, modification, should be done at the "reducer" level. The view should only care about dispatching actions to the reducer and not adding or removing values from an immutable.js collection.