6

I'm using Immutable.js with Redux in my React Native app.

Metadata, such as lookup tables, are fetched from a server and persisted locally in the app as Immutable.Map. The keys for the lookup values are integers (the primary key in the db).

When I fetch the data all integer keys are coerced into strings. This is normal behaviour for js objects. Thus, when a lookup Map is created, the keys will be strings.

Example:

let colors = Immutable.Map({
  '10': 'yellow',
  '20': 'pink',
  ..
})

The objects that are referencing the lookup values has the reference stored as numeric like this:

let michael = Immutable.Map({
  name: 'Michael',
  colorId: 10
})

Since Immutable.js does not coerce numeric keys to string I cannot do this:

let color = colors.get(michael.get('colorId'))

The above is the same as:

let color = colors.get(10)

This won't work, because the id is a string. This would work:

let color = colors.get('10')

I usually use Immutable.List to store datasets fetched from the server. To get an item I use find(). But the small lookup tables (essentially app metadata) are accessed very often and need to be speedy.

How do you solve this? Here are some alternatives to consider:

1. Manually convert keys to strings when searching

let color = colors.get(michael.get('colorId') + '') 

cons:

  • Not an attractive solution. Seems error prune to me.

2. Use plain js objects for lookup lists

pros:

  • Object key lookup are fast in js
  • Automatic coercion of keys to strings, both ways (writing and reading)

cons:

  • Ideally I'd like to use Immutable.js everywhere.

3. Use List.find() for lookup tables

pros:

  • Same concept as for the big datasets

cons:

  • I don't know the performance impact of using List.find() instead of Map.get()

4. Carefully craft the Immutable.Map to use numeric key

let colors = new Immutable.Map()
  .set(10, 'yellow')
  .set(20, 'pink')

pros:

  • Keys in the maps will match the value referencing the key (i.e. both are numeric)

cons:

  • cumbersome
  • Keys are already coerced to strings in the json object sent over the wire, so I'd essentially be converting them back to numeric (I think)

Edit.. did some performance tests

Interesting data from a simple performance test.

  • Lookup list of 10 items
  • Find a value one million times
  • Mac mini core i7 2.6

Immutable.Map with numeric keys: 185 ms

Immutable.List using find(): 972 ms

Plain JS object with coerced keys: 8 ms

Plain JS array using find(): 127 ms

As I'm using React Native I always have to look out for the 16 ms limit if I want to achieve 60 fps. The benchmark values does not seem to be linear. Running the test with only 100 lookups takes 1 ms with Map and 2 ms with List. That's quite expensive.

Have I done something wrong in the benchmarks? Otherwise I guess I'll have to leave Immutable.js out of this for now :(

Test code

let Immutable = require('immutable');

let mapTest = Immutable.Map()
  .set(1, Immutable.Map({value: 'one'}))
  .set(2, Immutable.Map({value: 'two'}))
  .set(3, Immutable.Map({value: 'three'}))
  .set(4, Immutable.Map({value: 'four'}))
  .set(5, Immutable.Map({value: 'five'}))
  .set(6, Immutable.Map({value: 'six'}))
  .set(7, Immutable.Map({value: 'seven'}))
  .set(8, Immutable.Map({value: 'eight'}))
  .set(9, Immutable.Map({value: 'nine'}))
  .set(10, Immutable.Map({value: 'ten'}));

let listTest = Immutable.fromJS([
  {key: 1,  value: 'one'},
  {key: 2,  value: 'two'},
  {key: 3,  value: 'three'},
  {key: 4,  value: 'four'},
  {key: 5,  value: 'five'},
  {key: 6,  value: 'six'},
  {key: 7,  value: 'seven'},
  {key: 8,  value: 'eight'},
  {key: 9,  value: 'nine'},
  {key: 10, value: 'ten'}
])

let objTest = {
  1:  {value: 'one'},
  2:  {value: 'two'},
  3:  {value: 'three'},
  4:  {value: 'four'},
  5:  {value: 'five'},
  6:  {value: 'six'},
  7:  {value: 'seven'},
  8:  {value: 'eight'},
  9:  {value: 'nine'},
  10: {value: 'ten'}
};

let arrayTest = [
  {key: 1,  value: 'one'},
  {key: 2,  value: 'two'},
  {key: 3,  value: 'three'},
  {key: 4,  value: 'four'},
  {key: 5,  value: 'five'},
  {key: 6,  value: 'six'},
  {key: 7,  value: 'seven'},
  {key: 8,  value: 'eight'},
  {key: 9,  value: 'nine'},
  {key: 10, value: 'ten'}
];

const runs = 1e6;
let i;
let key;
let hrStart;

console.log(' ')
console.log('mapTest -----------------------------')
key = 1;
hrstart = process.hrtime();
for(i=0; i<runs; i++) {
  let result = mapTest.getIn([key, 'value'] )
  key = (key >= 10) ? 1 : key + 1;
}
hrend = process.hrtime(hrstart);
console.info("Execution time (hr): %dms", hrend[0] * 1000 + hrend[1]/1000000);


console.log(' ')
console.log('listTest -----------------------------')
key = 1;
hrstart = process.hrtime();
for(i=0; i<runs; i++) {
  let result = listTest
    .find(item => item.get('key') === key)
    .get('value');
  key = (key >= 10) ? 1 : key + 1;
}
hrend = process.hrtime(hrstart);
console.info("Execution time (hr): %dms", hrend[0] * 1000 + hrend[1]/1000000);

console.log(' ')
console.log('arrayTest -----------------------------')
key = 1;
hrstart = process.hrtime();
for(i=0; i<runs; i++) {
  let result = arrayTest
    .find(item => item.key === key)
    .value

  key = (key >= 10) ? 1 : key + 1;
}
hrend = process.hrtime(hrstart);
console.info("Execution time (hr): %dms", hrend[0] * 1000 + hrend[1]/1000000);


console.log(' ')
console.log('objTest -----------------------------')
key = 1;
hrstart = process.hrtime();
for(i=0; i<runs; i++) {
  let result = objTest[key].value
  key = (key >= 10) ? 1 : key + 1;
}
hrend = process.hrtime(hrstart);
console.info("Execution time (hr): %dms", hrend[0] * 1000 + hrend[1]/1000000);
Michael
  • 1,764
  • 2
  • 20
  • 36
  • *'When I fetch the data all integer keys are coerced into strings. This is normal behaviour for js objects."* - why? – evolutionxbox Feb 02 '17 at 10:15
  • I don't know the thoughts behind this. But that's the way it is. {1: 'one'} will be coerced to {'1': 'one}. – Michael Feb 02 '17 at 10:19
  • Did you come up with a solution to this? – four-eyes Nov 15 '18 at 11:30
  • 1
    Solution for what? The coercion is what it is. It’s a JS thing. But I usually create Immutable.map via the array syntax to keep the integer key, like this: const myMap = Immutable.Map([[key, value], [key, value]]). If key is an integer it will be kept that way. – Michael Nov 15 '18 at 11:36

0 Answers0