Last active
October 11, 2018 20:17
-
-
Save bminer/e9e6f06d80d0de0792cd026d68d9b7ec to your computer and use it in GitHub Desktop.
Utility to make React `onChange` event handlers more enjoyable to write
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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