Skip to content

Instantly share code, notes, and snippets.

@tompng
Last active December 2, 2020 17:38
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 tompng/e31316f67be77398fd3c11868086e5f0 to your computer and use it in GitHub Desktop.
Save tompng/e31316f67be77398fd3c11868086e5f0 to your computer and use it in GitHub Desktop.
SelectableStateとselectorが使えるContextみたいなやつ
type SelectorFunction<T, U = any> = (data: T) => U
type SubscribeCallback<T = any> = (value: T) => void
type CompareFunction = (a: any, b: any) => boolean
export class SelectableState<T> {
constructor(public data: T) {}
selectorsInfo = new Map<SelectorFunction<T>, { value: any, comparators: Map<CompareFunction, Set<SubscribeCallback>> }>()
subscribe<U>(selector: SelectorFunction<T, U>, comparator: CompareFunction, callback: SubscribeCallback<U>) {
let info = this.selectorsInfo.get(selector)
if (!info) this.selectorsInfo.set(selector, info = { value: selector(this.data), comparators: new Map() })
let callbacks = info.comparators.get(comparator)
if (!callbacks) info.comparators.set(comparator, callbacks = new Set())
callbacks.add(callback)
return info.value
}
replace(data: T) {
if (this.data === data) return
this.data = data
this.selectorsInfo.forEach((info, selector) => {
const oldValue = info.value
const newValue = selector(data)
info.value = newValue
info.comparators.forEach((callbacks, comparator) => {
if (!comparator(oldValue, newValue)) callbacks.forEach(callback => callback(newValue))
})
})
}
unsubscribe<U>(selector: SelectorFunction<T, U>, comparator: CompareFunction, callback: SubscribeCallback<U>) {
const { comparators } = this.selectorsInfo.get(selector)
const callbacks = comparators.get(comparator)
callbacks.delete(callback)
if (callbacks.size !== 0) return
comparators.delete(comparator)
if (comparators.size !== 0) return
this.selectorsInfo.delete(selector)
}
}
import { isEqual } from 'lodash'
import React, { createContext, useContext, useMemo, useEffect, useRef, useReducer } from 'react'
class SelectableContext<T extends object> {
constructor(public Context: React.Context<SelectableState<T> | null>, public Provider: React.FC<{ data: T }>) {}
}
export function createSelectableContext<T extends object>() {
const Context = createContext<SelectableState<T> | null>(null)
const Provider: React.FC<{ data: T }> = ({ data, children }) => {
const state = useMemo(() => new SelectableState(data), [])
useEffect(() => state.replace(data), [data])
return <Context.Provider value={state}>{children}</Context.Provider>
}
return new SelectableContext<T>(Context, React.memo(Provider))
}
export function useSelector<T extends object, U>(ctx: SelectableContext<T>, selector: (state: T) => U, comparator: CompareFunction = eqeqeq): U {
const state: SelectableState<T> = useContext(ctx.Context)
const [, forceRender] = useReducer(n => n + 1, 0)
const dataRef = useRef<T>()
const valueRef = useRef<U>()
const selectorRef = useRef<(state: T) => U>()
let selectedValue: U
if (dataRef.current === state.data && selectorRef.current === selector) {
selectedValue = valueRef.current
} else {
const v = selector(state.data)
selectedValue = isEqual(valueRef.current, v) ? valueRef.current : v
}
useEffect(() => {
selectorRef.current = selector
const callback = (v: U) => {
dataRef.current = state.data
if (isEqual(valueRef.current, v)) return
valueRef.current = v
forceRender()
}
state.subscribe(selector, comparator, callback)
dataRef.current = state.data
selectorRef.current = selector
valueRef.current = selectedValue
return () => state.unsubscribe(selector, comparator, callback)
}, [selector])
return selectedValue
}
export function eqeqeq(a: any, b: any) { return a === b }
export function shallowEqual(a: unknown, b: unknown) {
if (a === b) return true
if (a == null || b == null || typeof a !== 'object' || typeof b !== 'object') return false
if (a.constructor !== b.constructor) return false
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
return true
} else {
const keys = [...Object.keys(a), ...Object.keys(b)]
return keys.every(k => a[k] === b[k])
}
}
export function deepEqual(a: unknown, b: unknown) {
return isEqual(a, b)
}
type ABCDEState = {
a: { b: number; c: number; d?: [number, number, { e: number } | undefined] }
}
const state = new SelectableState<ABCDEState>({a: { b: 1, c: 2, d: [1, 2, { e: 3 }]}})
state.subscribe(data => data.a.b+data.a.c, deepEqual, v => console.log('selector1 changed to:', v))
state.subscribe(data => data.a.d?.[2]?.e, deepEqual, v => console.log('selector2 changed to:', v))
state.replace({ a: { b: 2, c: 1 , d: [0, 0, { e: 3 }]} })
state.replace({ a: { b: 2, c: 1 , d: [0, 0, { e: 2 }]} })
state.replace({ a: { b: 1, c: 1 , d: [0, 0, { e: 2 }]} })
state.replace({ a: { b: 1, c: 1 } })
state.replace({ a: { b: 0, c: 2 , d: [0, 0, undefined]} })
state.replace({ a: { b: 0, c: 2 , d: [0, 0, { e: 1 }]} })
state.replace(JSON.parse(JSON.stringify(state.data)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment