Last active
December 2, 2020 17:38
-
-
Save tompng/e31316f67be77398fd3c11868086e5f0 to your computer and use it in GitHub Desktop.
SelectableStateとselectorが使えるContextみたいなやつ
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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