Skip to content

Instantly share code, notes, and snippets.

@bminer
Last active October 11, 2018 20:17
Show Gist options
  • Save bminer/e9e6f06d80d0de0792cd026d68d9b7ec to your computer and use it in GitHub Desktop.
Save bminer/e9e6f06d80d0de0792cd026d68d9b7ec to your computer and use it in GitHub Desktop.
Utility to make React `onChange` event handlers more enjoyable to write
/* Returns a function `(value, merge) => {...}` that calls
`component.setState(...)` to update the component's state with the new value
for `key`.
A simple example:
const func = updateKey(component, "counter");
func(23);
which is equivalent to:
component.setState({counter: 23});
`key` can be a nested path into an Object delimited by `.` For example:
updateKey(comp, "a.b.c")(value)
is equivalent to:
comp.setState({
a: {
...comp.state.a,
b: {
...comp.state.a.b,
c: value
}
}
})
Note that `a` and `b` are shallowly cloned to prevent direct mutations to
`comp.state`. One main purpose of this function is to eliminate a lot of
bookkeeping associated with nested object cloning.
There is also a `merge` argument that allows the `value` to be merged into the
Object located at `key`. For example:
updateKey(comp, "a.b")(value, true) // explicitly merge
updateKey(comp, "a.b", {merge: true})(value) // merge by default
updateKey(comp, "a.b", {merge: false})(value, true) // merge; ignore default
are all equivalent to:
comp.setState({
a: {
...comp.state.a,
b: {
...comp.state.a.b,
...value
}
}
})
`opts` can be passed to `updateKey` to change some behavior:
- `merge` - Sets the default `merge` value for the returned function
- `cb` - Callback to be passed to `setState` and called after setting state.
*/
export function updateKey(component, key, opts) {
opts = opts || {};
// Return a function that calls `setState` with custom updater and `cb`
return (value, merge) =>
component.setState((state, props) => {
// Split the `key` using "." as delimiter to determine path
const path = key.split(".");
// Create a `newState` object to be returned
const newState = {};
// `state` will point to the current Object in `state`
// `obj` will point to the current Object in `newState`
let obj = newState;
// `i` will keep track of how deep the Object `path` is
let i;
// Deep clone everything along the path to make React happy and
// avoid mutating `this.state` directly
for (i = 0; i < path.length - 1; i++) {
const target = state[path[i]] instanceof Array ? [] : {};
obj[path[i]] = Object.assign(target, state[path[i]]);
// Go deeper into `obj` and `state`
state = state[path[i]] || {}; // Note: use {} as fail-safe
obj = obj[path[i]];
}
// Update the value on the deeply cloned Object
// There are 2 options: merge Object state or replace state
if (merge === undefined) merge = opts.merge;
if (merge && typeof value === "object") {
// Merge state
const target = state[path[i]] instanceof Array ? [] : {};
obj[path[i]] = Object.assign(target, state[path[i]], value);
} else {
// Replace state
obj[path[i]] = value;
}
return newState;
}, opts.cb);
}
/* Returns a function `(e) => {...}` that calls `component.setState(...)` to
update the component's state with the new value for `key` using
`e.target.value` as the value. See `updateKey` above for more details.
If `e.target` has a `name` property, `{[e.target.name]: e.target.value}` will
be merged into `key` instead of replacing the value at `key` with
`e.target.value`.
Usage in React `render()` function:
<input onChange={updateKeyEvent(this, "counter")} />
In the above code, when the <input> is changed, the state's "counter" key will
be updated to match the value of the <input>.
Another example:
<input
name="d"
onChange={updateKeyEvent(this, "a.b.c")}
/>
This is roughly equivalent to this monstrosity:
<input
onChange={(e) =>
this.setState({
a: {
...comp.state.a,
b: {
...comp.state.a.b,
c: {
...comp.state.a.b.c,
d: e.target.value
}
}
}
})
}
/>
In the above code, when the <input> is changed, `this.setState(...)` will be
called such that `this.state.a.b.c` is merged with `{d: value}` where `value`
matches the current value of the <input>.
`opts` can be passed to change some behavior:
- `preventDefault` - By default, `e.preventDefault()` is called. To prevent
this call, set `preventDefault` explicitly to `false`
... All other `opts` for `updateKey`
*/
export function updateKeyEvent(component, key, opts) {
opts = opts || {};
const update = updateKey(component, key, opts);
if (opts.preventDefault !== false) {
// Call `preventDefault` by default unless explicitly set otherwise
return e => {
e.preventDefault();
const { name, value } = e.target;
if (name) {
// Special case: merge Object into `key`
return update({ [name]: value }, true);
} else {
return update(value);
}
};
} else {
return e => update(e.target.value);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment