Skip to content

Instantly share code, notes, and snippets.

@Phryxia
Last active February 7, 2022 17:02
Show Gist options
  • Save Phryxia/a21f430ade7d4796ef3f71dc50005421 to your computer and use it in GitHub Desktop.
Save Phryxia/a21f430ade7d4796ef3f71dc50005421 to your computer and use it in GitHub Desktop.
Custom react hook for executing batch promises and retrieve its result in asynchronous way with minimum rendering.
import { useEffect, useReducer, useRef, useState } from 'react'
interface DialationState {
counter: number
called: number
resolved: number
timer?: NodeJS.Timeout
isCalling: boolean
}
const SEC_IN_MILLISECONDS = 1000
// divide 'evaluators' and execute 'callsPerSecond' of them per second
// return object of which key is 'keyMaker(result)'
// isCalling: true if there are unevaluated promises
// isWaiting: true if there are unresolved promises
export function useDialatedPromises<T, K extends string | number | symbol>(
evaluators: (() => Promise<T>)[],
callsPerSecond: number,
keyMaker: (value: T) => K,
): {
currentResults: Partial<Record<K, T>>
abort: () => void
isCalling: boolean
isWaiting: boolean
} {
const [, update] = useReducer(() => ({}), {})
const [results, setResults] = useState<Partial<Record<K, T>>>({})
const dialationState = useRef<DialationState>({
counter: 0,
called: 0,
resolved: 0,
isCalling: true,
})
useEffect(() => {
function loop(): void {
const subEvaluators = evaluators.slice(
dialationState.current.counter,
dialationState.current.counter + callsPerSecond,
)
dialationState.current.called += subEvaluators.length
// you don't need to update when isCalling === true
if (!dialationState.current.isCalling) update()
subEvaluators.forEach(async evaluator => {
try {
const result = await evaluator()
dialationState.current.resolved += 1
setResults(results => ({ ...results, [keyMaker(result)]: result }))
} catch {
dialationState.current.resolved += 1
if (!dialationState.current.isCalling) update()
}
})
if (dialationState.current.isCalling && dialationState.current.counter + callsPerSecond < evaluators.length) {
dialationState.current.counter += callsPerSecond
dialationState.current.timer = setTimeout(loop, SEC_IN_MILLISECONDS)
} else {
dialationState.current.isCalling = false
update()
}
}
loop()
return () => {
if (dialationState.current.timer) clearTimeout(dialationState.current.timer)
dialationState.current.counter = 0
dialationState.current.timer = undefined
dialationState.current.isCalling = true
setResults({})
}
}, [evaluators])
function abort(): void {
if (dialationState.current.timer) clearTimeout(dialationState.current.timer)
dialationState.current.timer = undefined
dialationState.current.isCalling = false
update()
}
return {
currentResults: results,
abort,
isCalling: dialationState.current.isCalling,
isWaiting: dialationState.current.isCalling || dialationState.current.resolved < dialationState.current.called,
}
}
@Phryxia
Copy link
Author

Phryxia commented Feb 7, 2022

Here is the example

import { useDialatedPromises } from './useDialatedPromises'

interface CustomersProps {
  customerIds: string[]
}

interface Response {
  id: string
  name: string
}

export default function Customers({ customerIds }: CustomersProps) {
  const { currentResults, isWaiting, abort } = useDialatedPromises(
    customerIds.map((id) => async () => {
      return (await fetch(`https://blah/${id}`)).json() as Promise<Response>
    }),
    10,
    (result) => result.id
  )

  const names = Object.keys(currentResults).map(
    (id) => currentResults[id]!.name
  )

  return (
    <div>
      <div>{isWaiting && 'is loading...'}</div>
      <div>{names.join(', ')}</div>
      <button onClick={abort}>stop</button>
    </div>
  )
}

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