Facebook just released immutable-js a persistent data structure library for JavaScript. Given the performance benefit immutable data can bring to React, it’s little surprise Facebook is investing in them. However the question remains how to deliver immutable data to clients given the prevalence of JSON.
In this post I’m going to demonstrate consuming plain JSON with transit-js and producing immutable-js values instead of JavaScript objects and arrays.
If you have Node.js installed you can follow the
code presented in the post easily on your machine. Create a directory
on your machine and add the following package.json
file to it:
{
"name": "immutable-json",
"version": "0.1.0",
"dependencies": {
"immutable": "2.0.3",
"transit-js": "0.8.670"
}
}
Then run the following at the command line to install the dependencies:
npm install
Create a JavaScript file and put the following requires at the top:
var Immutable = require("immutable"),
transit = require("transit-js");
transit-js exposes two low-level options arrayBuilder
and
mapBuilder
for constructing readers. This allows readers to
interpret the meaning of the Transit
array and map encodings.
We can customize a reader to return Immutable.Vector
and Immutable.Map
like so:
var rdr = transit.reader("json", {
arrayBuilder: {
init: function(node) { return Immutable.Vector().asMutable(); },
add: function(ret, val, node) { return ret.push(val); },
finalize: function(ret, node) { return ret.asImmutable(); },
fromArray: function(arr, node) { return Immutable.Vector.from(arr); }
},
mapBuilder: {
init: function(node) { return Immutable.Map().asMutable(); },
add: function(ret, key, val, node) { return ret.set(key, val); },
finalize: function(ret, node) { return ret.asImmutable(); }
}
});
Note that the builder methods get the original JSON node as contextual
information. By default transit-js builds the values
incrementally. transit-js can also build values at once from an array
as in the case of Immutable.Vector
. Sadly this can’t be done for
Immutable.Map
yet. transit-js maps and ClojureScript both have an
array map type for maps with less than or equal to 8 keys and it is a
significant performance enhancement in time and space.
We can now read JSON objects and arrays into immutable maps and vectors:
rdr.read("[1,2,3]"); // Vector [ 1, 2, 3 ]
rdr.read('{"foo":"bar"}'); // Map { foo: "bar" }
For writing we need to make write handlers. This is also pretty straightforward:
var VectorHandler = transit.makeWriteHandler({
tag: function(v) { return "array"; },
rep: function(v) { return v; },
stringRep: function(v) { return null; }
});
var MapHandler = transit.makeWriteHandler({
tag: function(v) { return "map"; },
rep: function(v) { return v; },
stringRep: function(v) { return null; }
});
var wrtr = transit.writer("json-verbose", {
handlers: transit.map([
Immutable.Vector, VectorHandler,
Immutable.Map, MapHandler
])
});
And now we can roundtrip:
wrtr.write(rdr.read("[1,2,3]")); // [1,2,3]
wrtr.write(rdr.read('{"foo":"bar"}')); // {"foo":"bar"}
If you’re excited about getting immutable data from your server into
your React client application,
transit-js presents a pretty good story even if you marshal plain
JSON. However, if you change your backend to emit Transit JSON you
will see a fairly significant performance boost. In the future
if Immutable provides more efficient means to construct values as well as
exposing an array map type, you will be able to deserialize immutable
values nearly as fast or faster than you can JSON.parse
plain JSON
data in many modern browsers.