Skip to content

Instantly share code, notes, and snippets.

@myndzi
Last active August 29, 2015 13:56
Show Gist options
  • Save myndzi/9026516 to your computer and use it in GitHub Desktop.
Save myndzi/9026516 to your computer and use it in GitHub Desktop.
React state update propagation helper
/* Call initially with arguments as a path from this.state
* to the key you want a callback to update:
*
* this will return a function to update this.state.foo:
* this.update('foo')
*
* and this will return one to update this.state[0].bar:
* this.update('state', 0, 'bar')
*
* All arguments are optional; this returns a callback
* that will directly update this.state:
*
* this.update()
*
* The callback can be called with an optional further path
* and a value to update the state at that path. The following
* code blocks accomplish the same thing:
*
* var callback = this.update('foo');
* callback('value');
*
* this.setState({ foo: 'value' })
*
* The new state object is a deep copy of this.state.
* You can optionally supply a further path when setting the
* value with the callback. the following code blocks are
* equivalent:
*
* var callback = this.update('foo');
* callback('bar', 'value');
*
* var newVal = { foo: { bar: 'value' } }
* this.setState($.extend(true, {}, this.state, newVal))
*
* callbacks can be extended later by calling the 'callback'
* method. 'foobar' and 'bar' are equivalent here:
*
* var foo = this.update('foo'),
* foobar = foo.callback('bar');
*
* var bar = this.update('foo', 'bar');
*
* Extending callbacks and specifying multiple arguments for
* paths are essentially equivalent. These all do the same thing:
*
* this.update('foo').callback('bar').callback('baz')('value');
* this.update('foo', 'bar')('baz', 'value');
* this.update('foo', 'bar', 'baz')('value');
* this.update().callback('foo', 'bar', 'baz')('value');
*
*/
function updateHelper(/* keys */) {
var i = 0, x = arguments.length, args = new Array(x);
for (;i < x; i++) { args[i] = arguments[i]; }
var newState = $.extend(true, { }, this.state),
noKey = { };
return createCallback.apply(this, [newState, newState].concat(args));
// state always points to a copy of the root state
// ptr points to the current subkey. we also hold
// onto the last key value so we can assign to the
// object reference given by ptr
function createCallback(state, ptr/*, args...*/) {
var i = 0, x = arguments.length,
state = arguments[i++],
ptr = arguments[i++],
key;
for (; i < x-1; i++) {
if (arguments[i] === noKey) {
continue;
} else {
ptr = ptr[arguments[i]];
}
}
var key = arguments[i++];
if (typeof key === 'undefined') { key = noKey; }
// no scope variables this time, new bindings every call
var cb = updateState.bind(this, state, ptr, key);
cb.callback = createCallback.bind(this, state, ptr, key);
return cb;
}
function updateState(state, ptr/*, args...*/) {
var i = 0, x = arguments.length,
state = arguments[i++],
ptr = arguments[i++];
for (; i < x-2; i++) { ptr = ptr[arguments[i]]; }
var key = arguments[i++],
val = arguments[i++];
if (typeof val === 'undefined') {
// never got a 'key'
state = key;
} else {
ptr[key] = val;
}
return this.setState(state);
}
}
'use strict';
var assert = require('assert'),
$ = { extend: require('jquery-extend') };
var origState = {
foo: [
{ bar: 'a' },
{ bar: 'b' }
],
keke: 'lar',
a: { b: { c: 'd' } }
}, origJson = JSON.stringify(origState);
function cmp(a, b) { return JSON.stringify(a) === JSON.stringify(b); }
var test = {
state: origState,
update: updateHelper,
setState: function (newState) { return newState; }
};
var res = test.update('keke')('hai');
assert(res !== origState);
assert(res.keke === 'hai');
var pre = test.update('keke'), res;
res = pre('lar');
assert(cmp(res, origState));
res = pre('lar');
assert(cmp(res, origState));
var res = test.update('foo', 0)('bar', 'unf');
assert(res.foo[0].bar === 'unf');
var res = test.update('foo').callback(0, 'bar')('unf');
assert(res.foo[0].bar === 'unf');
assert(res.foo.length === 2);
assert(res.keke === 'lar');
var res = test.update('a').callback('b').callback('c')('d');
assert(cmp(res, origState));
var res = test.update('a', 'b')('c', 'd');
assert(cmp(res, origState));
var res = test.update('a', 'b', 'c')('d');
assert(cmp(res, origState));
var res = test.update().callback('a', 'b', 'c')('d');
assert(cmp(res, origState));
console.log('done');
@myndzi
Copy link
Author

myndzi commented Feb 16, 2014

And a rather more compact version of the above for use in a react component:

update: function () {
    var self = this, newCB = true, path = [ ],
        newState = $.extend(true, { }, this.state);

    var cb = function () {
        var i = 0, x = arguments.length;
        if (arguments[0] === true) { i = 1; newCB = true; }
        for (;i < x; i++) { path.push(arguments[i]); }

        if (newCB) { newCB = false; return cb; }
        if (arguments.length === 0) { return cb; }
        if (path.length === 1) { return self.setState(path[0]); }

        var ptr = newState,
            val = path.pop(),
            key = path.pop();

        while (path.length) { ptr = ptr[path.shift()]; }

        ptr[key] = val;
        return self.setState(newState);
    };

    return cb.apply(null, arguments);
},

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment