Last active
June 27, 2020 21:55
-
-
Save agriffis/b0b48923130bd30a2ec4e6bddeb2e629 to your computer and use it in GitHub Desktop.
usePromises React hook
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
import React from 'react' | |
/** | |
* React hook to force a re-render, for hooks with fancy state management. | |
*/ | |
export const useForceRender = () => { | |
const [, emit] = React.useState() | |
// Stable function response, like useCallback without checking. | |
return React.useRef(() => { | |
emit({}) // unique obj to force | |
}).current | |
} |
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
import * as R from 'ramda' | |
import React from 'react' | |
import {useForceRender} from './useForceRender' | |
/** | |
* Check if two arrays contain the same set of values, regardless of order or | |
* duplicates. https://stackoverflow.com/a/55597200/347386 | |
*/ | |
export const eqValues = R.curryN(2, R.compose(R.isEmpty, R.symmetricDifference)) | |
/** | |
* Check if two objects contain the same set of keys. | |
*/ | |
const eqKeys = R.curryN( | |
2, | |
R.compose(R.apply(eqValues), R.unapply(R.map(R.keys))), | |
) | |
/** | |
* React hook for resolving promises. | |
* | |
* const promised = usePromises({ | |
* one: () => Promise.resolve('yay'), | |
* two: () => Promise.resolve('hang on'), | |
* three: false && (() => Promise.resolve('later')), | |
* four: () => Promise.reject('whoops'), | |
* }) | |
* | |
* Given an object of key-promise pairs, returns an object of those promises | |
* plus their current status (all start pending). Each time a promise completes, | |
* returns current statuses: | |
* | |
* { | |
* one: {promise: p1, status: 'resolved', result}, | |
* two: {promise: p2, status: 'pending'}, | |
* three: {promise: false, status: 'falsy'}, | |
* four: {promise: p4, status: 'rejected', error}, | |
* } | |
* | |
* In the spirit of react-query, any promise can be falsy, and it's also valid | |
* to pass an empty object or falsy value for the entire object. | |
* | |
* The object of promises is allowed to change between calls to usePromises. New | |
* keys will be added to state, missing keys will be dropped. If the value | |
* associated with a key changes from a truthy value to any other value, this | |
* will be ignored so that the caller doesn't need to make sure values are | |
* identical. | |
* | |
* @param {Object} promises - named promises | |
* @return {Object} statuses - named {promise, status, result, error} | |
*/ | |
export const usePromises = promises => { | |
const self = React.useRef({active: true, state: {}}).current | |
const force = useForceRender() | |
// Prevent outstanding promises from calling dispatch when the component is | |
// unmounted. | |
React.useEffect( | |
() => () => { | |
self.state = {} | |
}, | |
[], // eslint-disable-line react-hooks/exhaustive-deps | |
) | |
// Build new state from incoming promises. Any missing keys will be | |
// unceremoniously dropped. Use a flag to track added values since it's faster | |
// than comparing afterward. | |
let updatedState = !eqKeys(promises, self.state) | |
const newState = R.mapObjIndexed((promise, k) => { | |
if ( | |
self.state[k] && | |
// Never transition back to falsy, and don't replace falsy status with | |
// another falsy status. | |
(self.state[k].promise || !promise) | |
) { | |
return self.state[k] | |
} | |
updatedState = true | |
return { | |
promise: typeof promise === 'function' ? promise() : promise, | |
status: promise ? 'init' : 'falsy', | |
} | |
}, promises) | |
if (updatedState) { | |
self.state = newState | |
// When a promise completes, update state and force rerender. | |
const put = (k, obj) => { | |
if (self.state[k]) { | |
self.state = { | |
...self.state, | |
[k]: { | |
...self.state[k], | |
...obj, | |
}, | |
} | |
force() | |
} | |
} | |
for (const [k, p] of Object.entries(self.state)) { | |
if (p.status === 'init') { | |
p.status = 'pending' | |
p.promise.then( | |
result => put(k, {status: 'resolved', result}), | |
error => put(k, {status: 'rejected', error}), | |
) | |
} | |
} | |
} | |
return self.state | |
} | |
/** | |
* Singular version of usePromises for convenience. | |
*/ | |
export const usePromise = promise => usePromises({pinky: promise}).pinky |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment