Last active
November 11, 2024 07:18
-
-
Save beorn/2726d3d3ffe661bb1c3666e2da51c787 to your computer and use it in GitHub Desktop.
Brainstorming different kinds of state mgmt APIs
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
// NOTE: I did use ChatGPT for help fleshing this out | |
import React from "react" | |
// Global variable to track current computation for dependency tracking | |
let currentComputation: ComputedObservable<any> | null = null | |
// Observable class to handle state changes and subscriptions | |
class Observable<T> { | |
#value: T | |
#subs = new Set<(value: T) => void>() | |
constructor(value: T) { | |
this.#value = value | |
} | |
get value(): T { | |
if (currentComputation) currentComputation.deps.add(this) | |
return this.#value | |
} | |
set value(newValue: T) { | |
if (!Object.is(this.#value, newValue)) { | |
this.#value = newValue | |
this.#notify() | |
} | |
} | |
subscribe(callback: (value: T) => void): () => void { | |
this.#subs.add(callback) | |
return () => void this.#subs.delete(callback) | |
} | |
#notify() { | |
for (const callback of this.#subs) callback(this.#value) | |
} | |
} | |
// ComputedObservable class to handle computed properties with dependencies | |
class ComputedObservable<T> { | |
#computeFn: () => T | |
#value!: T | |
deps = new Set<Observable<any>>() | |
#depUnsubs = new Map<Observable<any>, () => void>() | |
#subs = new Set<(value: T) => void>() | |
#isComputing = false | |
#pendingComputation = false | |
constructor(computeFn: () => T) { | |
this.#computeFn = computeFn | |
this.#computeValue() | |
} | |
#computeValue() { | |
if (this.#isComputing) return | |
this.#isComputing = true | |
try { | |
const oldValue = this.#value | |
this.#clearDeps() | |
currentComputation = this | |
const newValue = this.#computeFn() | |
currentComputation = null | |
if (!Object.is(oldValue, newValue)) { | |
this.#value = newValue | |
for (const callback of this.#subs) callback(this.#value) | |
} | |
this.#setupDeps() | |
} finally { | |
this.#isComputing = false | |
this.#pendingComputation = false | |
} | |
} | |
#clearDeps() { | |
for (const unsub of this.#depUnsubs.values()) unsub() | |
this.deps.clear() | |
this.#depUnsubs.clear() | |
} | |
#setupDeps() { | |
for (const dep of this.deps) { | |
const unsub = dep.subscribe(() => this.#depChanged()) | |
this.#depUnsubs.set(dep, unsub) | |
} | |
} | |
get value(): T { | |
if (currentComputation && currentComputation !== this) { | |
currentComputation.deps.add(this as any) | |
} | |
return this.#value | |
} | |
#depChanged() { | |
if (!this.#pendingComputation) { | |
this.#pendingComputation = true | |
Promise.resolve().then(() => { | |
if (this.#pendingComputation) this.#computeValue() | |
}) | |
} | |
} | |
subscribe(callback: (value: T) => void): () => void { | |
this.#subs.add(callback) | |
return () => void this.#subs.delete(callback) | |
} | |
} | |
// Add type helper for store definition | |
type Store<T> = { | |
[K in keyof T]: T[K] extends (...args: any[]) => any | |
? Observable<T[K]> | |
: T[K] extends { get: () => any } | |
? ComputedObservable<ReturnType<T[K]["get"]>> | |
: Observable<T[K]> | |
} | |
// Update createStore to use type inference | |
export function createStore<T extends object>(initialState: T): Store<T> { | |
const store: any = {} | |
const observables: { [key: string]: Observable<any> } = {} | |
const computedObservables: { [key: string]: ComputedObservable<any> } = {} | |
// Proxy to handle 'this' binding and property access within computed properties and methods | |
const storeProxy = new Proxy(store, { | |
get(target, prop) { | |
// Access observables directly without going through .value | |
if (prop in observables) { | |
return observables[prop as string].value | |
} else if (prop in computedObservables) { | |
return computedObservables[prop as string].value | |
} else if (prop in target) { | |
return target[prop as string] | |
} | |
return undefined | |
}, | |
set(_target, prop, value) { | |
if (prop in observables) { | |
observables[prop as string].value = value | |
return true | |
} | |
return false | |
}, | |
}) | |
const propertyDescriptors: Record<keyof T, PropertyDescriptor> = | |
Object.getOwnPropertyDescriptors(initialState) | |
// Process initial state properties | |
for (const key in propertyDescriptors) { | |
const descriptor = propertyDescriptors[key] | |
const value = initialState[key] | |
if (typeof descriptor.get === "function") { | |
// Computed property | |
// Will be processed after creating the proxy | |
} else if (typeof value === "function") { | |
// Method | |
const method = value | |
const observableMethod = new Observable(function (...args: any[]) { | |
return method.apply(storeProxy, args) | |
}) | |
observables[key] = observableMethod | |
Object.defineProperty(store, key, { | |
get() { | |
return observableMethod | |
}, | |
}) | |
} else { | |
// Observable property | |
observables[key] = new Observable(value) | |
Object.defineProperty(store, key, { | |
get() { | |
return observables[key] | |
}, | |
set(value) { | |
observables[key].value = value | |
}, | |
}) | |
} | |
} | |
// Now process computed properties | |
for (const key in propertyDescriptors) { | |
const descriptor = propertyDescriptors[key] | |
if (typeof descriptor.get === "function") { | |
const computeFn = function () { | |
return descriptor.get!.call(storeProxy) | |
} | |
const computedObservable = new ComputedObservable(computeFn) | |
computedObservables[key] = computedObservable | |
Object.defineProperty(store, key, { | |
get() { | |
return computedObservable | |
}, | |
}) | |
} | |
} | |
return store as Store<T> | |
} | |
// useStoreValue hook to get the current value and subscribe to changes | |
function useStoreValue<T>( | |
observable: Observable<T> | ComputedObservable<T> | |
): T { | |
const [, forceUpdate] = React.useReducer((x) => x + 1, 0) | |
React.useEffect(() => observable.subscribe(forceUpdate), [observable]) | |
return observable.value | |
} | |
// --- | |
// Example usage: | |
// Create the store | |
const store = createStore({ | |
a: 1, | |
b: 2, | |
get sum() { | |
return this.a + this.b | |
}, | |
get sum2x() { | |
return this.sum * 2 | |
}, | |
addA(n: number) { | |
this.a = this.a + this.sum + n | |
}, | |
}) | |
// In a React component | |
export default function MyComponent() { | |
const a = useStoreValue(store.a) | |
const b = useStoreValue(store.b) | |
const sum = useStoreValue(store.sum) | |
const sum2x = useStoreValue(store.sum2x) | |
const addA = useStoreValue(store.addA) | |
return ( | |
<div> | |
<h1>Brainstorming 9</h1> | |
<div>a: {a}</div> | |
<div>b: {b}</div> | |
<div>sum: {sum}</div> | |
<div>sum2x: {sum2x}</div> | |
<button onClick={() => addA(5)}>Add to A</button> | |
</div> | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment