Skip to content

Instantly share code, notes, and snippets.

@jonastreub
Last active April 2, 2019 15:40
Show Gist options
  • Save jonastreub/14a876b80ea8d89719d30ed22cf02248 to your computer and use it in GitHub Desktop.
Save jonastreub/14a876b80ea8d89719d30ed22cf02248 to your computer and use it in GitHub Desktop.
Share values among components using a hook
import * as React from "react"
// Share values among components using a hook. The values are available using an identifier.
// There are currently three types of build-in tokens:
// - useNumberToken
// - useStringToken
// - useBooleanToken
// - useObjectToken
// - useArrayToken
// Example using hardcoded identifier:
// const [filter] = useStringToken("listFilter")
// Example using dynamic identfier:
// const [value, setValue] = useNumberToken(identifier, 0)
export class ReferenceCountedStore<T> {
private _value: T
private deleter: () => void
constructor(initialValue: T, deleter: () => void) {
this._value = initialValue
this.deleter = deleter
}
get value() {
return this._value
}
set value(newValue: T) {
this._value = newValue
this.updateHandlers.forEach(update => update())
}
private updateHandlers: (() => void)[] = []
addUpdateHandler(update: () => void) {
this.updateHandlers.push(update)
}
removeUpdateHandler(update: () => void) {
this.updateHandlers = this.updateHandlers.filter(
handler => handler !== update
)
if (this.updateHandlers.length === 0) {
this.deleter()
}
}
}
export class TokenManager<T> {
stores = new Map<string, ReferenceCountedStore<T>>()
getStore(identifier: string, initialValue?: T) {
if (this.stores.has(identifier)) {
return this.stores.get(identifier)
} else {
const store = new ReferenceCountedStore<T>(initialValue, () =>
this.deleteStore(identifier)
)
this.stores.set(identifier, store)
return store
}
}
private deleteStore(identifier: string) {
this.stores.delete(identifier)
}
}
export function generateUseToken<T>(
manager: TokenManager<T>,
isValidValue: (value: any) => boolean,
defaultInitialValue: T
) {
return function(
identifier: string,
initialValue?: T
): [T, (newValue: T) => void] {
const [_, setUpdate] = React.useState(0)
const store = manager.getStore(
identifier,
isValidValue(initialValue) ? initialValue : defaultInitialValue
)
React.useEffect(() => {
function updateHandler() {
setUpdate(current => current + 1)
}
store.addUpdateHandler(updateHandler)
return () => store.removeUpdateHandler(updateHandler)
}, [store])
return [
store.value,
newValue => {
if (isValidValue(newValue)) store.value = newValue
},
]
}
}
// Managers
const numberTokenManager = new TokenManager<number>()
const stringTokenManager = new TokenManager<string>()
const booleanTokenManager = new TokenManager<boolean>()
const objectTokenManager = new TokenManager<{ [key: string]: any }>()
const arrayTokenManager = new TokenManager<any[]>()
// API
export const useNumberToken = generateUseToken(numberTokenManager, isNumber, 0)
export const useStringToken = generateUseToken(stringTokenManager, isString, "")
export const useBooleanToken = generateUseToken(
booleanTokenManager,
isBoolean,
true
)
export const useObjectToken = generateUseToken(objectTokenManager, isObject, {})
export const useArrayToken = generateUseToken(arrayTokenManager, isArray, [])
// Validators
function isNumber(value: any) {
return typeof value === "number" && Number.isFinite(value)
}
function isBoolean(value: any) {
return value === true || value === false
}
function isString(value: any) {
return typeof value === "string"
}
function isObject(value: any) {
return typeof value === "object" && value && !Array.isArray(value)
}
function isArray(value: any) {
return typeof value === "object" && value && Array.isArray(value)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment