Immutable/functional select/update queries for plain JS.


Keywords
immutable, select, update, query
License
MIT
Install
npm install qim@0.0.52

Documentation

qim

Functional style immutability for plain JS with special query sauce.

What?

Let's start with some data like this:

const users = {
  mary: {
    name: {
      first: 'Mary',
      last: 'Bar'
    },
    friends: [],
    balance: 1000
  },
  {
    name: {
      first: 'Joe',
      last: 'Foo'
    },
    friends: [],
    balance: 100
  }
};

qim has a bunch of helpers like this that make it easy to reach in and modify an object:

import {setIn, updateIn, pushIn} from 'qim';

const newUsers1 = setIn(['joe', 'name', 'first'], 'Joseph', users);
const newUsers2 = updateIn(['mary', 'balance'], bal => bal + 10, users);
const newUsers3 = pushIn(['joe', 'friends'], 'mary', users);

And of course, these modifications are immutable, but they share unmodified branches:

console.log(newUsers1 !== users);
// true
console.log(newUsers1.mary === users.mary);
// true

Changing something to its current value is a no-op:

const newUsers = setIn(['mary', 'name', 'first'], 'Mary', users);
console.log(newUsers === users);
// true

Okay, now let's make things more interesting. Let's increase everyone's balance by 10.

import {$eachValue} from 'qim';

const newUsers = updateIn([$eachValue, 'balance'], bal => bal + 10);

Each part of the path in qim functions is actually a "navigator". Strings navigate to keys. $eachValue is a navigator that navigates to each value of an array or object. Kind of like mapValues from lodash, but navigators in qim are only worried about what they navigate to, never about anything they don't navigate to. Let's see what that means.

Let's say we want to increase everyone's balance by 10, but only if the balance is 500 or greater. Hmm, that sounds like a map and a filter. But we want to modify the object, so we can't really filter. We have to do something like this:

import {mapValues} from 'lodash/fp';

const newUsers = mapValues(
  user => {
    if (user.balance < 500) {
      return user;
    }
    return {
      ...user,
      balance: user.balance + 10
    };
  }
, users);

This is a simple example, but there are already a couple problems here. We have to worry about returning users that we don't actually touch, and we have to worry about the rest of the user properties that we don't touch. With qim, you can do this instead:

const newUsers = updateIn([$eachValue, 'balance', bal => bal >= 500], bal => bal + 10, users);

Here we introduce a predicate selector. Any function that appears in a path acts as a filter, and we only continue navigating if the predicate passes. But we don't have to worry about the unfiltered users. Those remain unchanged. And we only have to worry about the balance property. Other properties are also unchanged.

These navigators are useful for selecting data too.

import {selectIn} from 'qim';

const names = selectIn([$eachValue, 'name', 'first'], users);
// ['Joe', 'Mary']

Let's get a little more fancy. Let's grab all the first names of people that have high balances.

import {hasIn} from 'qim';

// All functions are curried, so you can leave off the data to get a function.
const hasHighBalance = hasIn(['balance', bal => bal >= 500]);

const names = selectIn([$eachValue, hasHighBalance, 'name', 'first']);
// ['Mary']

hasIn checks if a selection returns anything. We use currying to create a function for checking if a user's balance is high, and we use that as a predicate to select first names of users with a high balance.

Cool, huh?