Skip to content

Instantly share code, notes, and snippets.

@DV8FromTheWorld
Created November 16, 2021 20:56
Show Gist options
  • Save DV8FromTheWorld/d422272be3681f117ea047345e0dff8c to your computer and use it in GitHub Desktop.
Save DV8FromTheWorld/d422272be3681f117ea047345e0dff8c to your computer and use it in GitHub Desktop.
Possible Vue Compostion Memoizer
/**
* Vue Composition HigherOrderFunction (HOF) that will take a Vue Composition function and return a
* memoized version of that function.
*
* A Memoized function in a function that, when given the same inputs, will skip any calculations done by the function
* and instead return a cached result that matches the inputs. This allows us to skip any duplicate computation
* or other calculations that would occur by calling the function from multiple places but with the same inputs.
*
* It is expected that the resolver function will take the provided inputs and convert them to a keyable value
* that can be used in the internal cache.
*
* @example
* function useStringValue(lengthLimit) {
* const val = ref("")
* const setVal = newVal => {
* if (newVal.length > lengthLimit)
* throw new Error("cannot set length longer than " + lengthLimit)
*
* val.value = newVal
* }
*
* return {
* val,
* setVal
* }
* }
*
* const memoizedFunction = createMemoizedComposable(useStringValue, (lengthLimit) => lengthLimit)
*
* const { val: valOne, setVal: setValOne } = memoizedFunction(20)
* const { val: valTwo, setVal: setValTwo } = memoizedFunction(10)
* const { val: valThree, setVal: setValThree } = memoizedFunction(20)
*
* // valOne and valThree are the _exact same reference_ of a Ref<string>
* // valTwo is a different Ref<string> because the arguments passed were different
*
* @template T
*
* @param {function(...*): T} composable
* @param {function(...*): *} resolver
* @returns {function(...*): T}
*/
export function createMemoizedComposable(composable, resolver) {
assert(composable, "Argument 'composable' is required")
assert(composable, "Argument 'resolver' is required")
const cache = new Map()
return function createSingletonScope(...args) {
let subscribers = 0
//If there is already a shared composable state for the provided args, return it
const cacheKey = resolver(...args)
if (cache.has(cacheKey)) {
subscribers++
return cache.get(cacheKey)
}
//If not, setup an effectScope so that we can manage the reactivity setup/teardown
// of this shared composable separate from the setup/teardown of the system using it.
//Run the composable inside the effect scope to capture the reactive state that we want
// to be able to shared between many systems.
let scope = effectScope(true)
const state = scope.run(() => composable(...args))
//Set the reactive state into the cache for future callers
cache.set(cacheKey, state)
//When any system using this shared composable disposes (for components, unmount), decrement our subscriber count.
//If there are no more subscribers to this variant of the shared composable, kill it off and remove from the cache.
onScopeDispose(() => {
if (scope && --subscribers <= 0) {
scope.stop()
scope = null
cache.delete(cacheKey)
}
})
//Return the reactive state of the shared composable
//This only happens if the cache is cold for the set of args provided
return state
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment