Skip to content

Instantly share code, notes, and snippets.

@agriffis
Last active June 27, 2020 21:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save agriffis/b0b48923130bd30a2ec4e6bddeb2e629 to your computer and use it in GitHub Desktop.
Save agriffis/b0b48923130bd30a2ec4e6bddeb2e629 to your computer and use it in GitHub Desktop.
usePromises React hook
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
}
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