Skip to content

Instantly share code, notes, and snippets.

@LoganDark
Last active December 9, 2021 21:22
Show Gist options
  • Save LoganDark/ed70f360542e3d462cb9721344eb531f to your computer and use it in GitHub Desktop.
Save LoganDark/ed70f360542e3d462cb9721344eb531f to your computer and use it in GitHub Desktop.
import {DependencyList, useCallback, useDebugValue, useRef} from 'react'
import useForceUpdate from 'use-force-update'
/*! Copyright © 2021 LoganDark
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Checks if two arrays are equal. This mirrors React's method of checking
* two {@link DependencyList}s for changes.
*
* @param arr1 The first array to compare.
* @param arr2 The second array to compare.
* @returns `true` if both arrays are equal, `false` otherwise. Equality takes
* into account the two arrays' lengths and reference equality of their content.
*/
function arraysEqual(arr1: readonly any[], arr2: readonly any[]) {
if (arr1.length !== arr2.length) return false
const len = arr1.length
for (let i = 0; i < len; i++)
if (arr1[i] !== arr2[i]) return false
return true
}
interface ScopedStateData<T> {
value: T,
deps: DependencyList
}
/**
* `useScopedState` is similar to React's {@link useState}, however it instantly
* (<1 state update) resets its value whenever its `deps` change.
*
* This is useful for storing state *pertaining to* a certain object without
* unnecessary re-renders. It's guaranteed to reset immediately when called with
* a `deps` list that differs from the previous render.
*
* `initializer`, if a function, is called whenever the state resets, being
* passed the old state. If you do not want to reset, simply return the argument
* passed.
*
* To detect the difference between the state initializing for the first time
* and the state resetting, use a vararg `(...args)` for your function and check
* its length. When the state first initializes, it calls the `initializer` with
* no arguments.
*
* @param initializer The initial value or initializer function to be used to
* prefill the state. For types (`T`) that are themselves functions, an
* initializer function must be used (for type safety reasons).
* @param deps The dependencies that will reset the state if changed.
* @returns A tuple containing the current value of the state and a function
* that updates the value of the state when called.
*/
export default function useScopedState<T>(initializer: Exclude<T, Function> | ((oldValue?: T) => T), deps: DependencyList) {
const ref = useRef<ScopedStateData<T>>(null as unknown as ScopedStateData<T>)
if (!ref.current || !arraysEqual(deps, ref.current.deps)) {
ref.current = {
value: typeof initializer === 'function'
// Consumers can use the number of arguments passed to
// `initializer` to tell if this is the first render or not.
// `oldValue` is simply omitted if not present.
// @ts-ignore
? ref.current ? initializer(ref.current.value) : initializer()
: initializer,
deps
}
}
const forceUpdate = useForceUpdate()
const update = useCallback((newValue: Exclude<T, Function> | ((oldValue: T) => T)) => {
const current = ref.current.value
if (typeof newValue === 'function') {
// @ts-ignore
newValue = newValue(current)
}
if (newValue !== current) {
ref.current.value = newValue as T
forceUpdate()
}
}, [ref, forceUpdate])
useDebugValue(ref.current.value)
return [ref.current.value, update] as const
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment