Example hook using a Proxy to subscribe to used properties of a store
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
import { useEffect } from "react"; | |
/** | |
* Exercises left to reader to implement: | |
* | |
* useResolvedStore | |
* getValueFromStore | |
* shouldUpdateForChange | |
* forceUpdate | |
* addChangeListener | |
* removeChangeListener | |
*/ | |
const BLANK_TARGET = Object.seal({}); | |
const BASE_PROXY_TRAPS = [ | |
"apply", | |
"construct", | |
"defineProperty", | |
"deleteProperty", | |
"get", | |
"getOwnPropertyDescriptor", | |
"getPrototypeOf", | |
"has", | |
"isExtensible", | |
"ownKeys", | |
"preventExtensions", | |
"set", | |
"setPrototypeOf", | |
].reduce( | |
(result, key) => ({ | |
...result, | |
[key]() { | |
throw new Error("Operation Not Allowed"); | |
}, | |
}), | |
{} | |
); | |
/** | |
* Use store values and auto-re-render when they change. | |
* Ex: | |
* ```js | |
* // Pass a store directly | |
* const {loading, items} = useStoreValue(myStore); | |
* // Select a store from context | |
* const {loading, items, anyStoreValue} = useStoreValue(store => store.isMyFavoriteStore); | |
* // Use the nearest store from context | |
* const {loading, items, sort} = useStoreValue(); | |
* // Also: use computed names and label friendly for local use: | |
* const {[Store.LOADING]: loading, [Store.SOME_VALUE]: data} = useStoreValue(); | |
* ``` | |
* | |
* @param {Store|Store[]|function(Store): boolean} [source] A store, an array of stores, or a function to select a store from context. The default resolves the store(s) from context. | |
* @returns {Record<string, any>} ephemeral store getter proxy. Do not retain a reference to this value. Pull values immediately and discard this proxy. | |
*/ | |
export function useStoreValue(source = Boolean) { | |
let locked = false; | |
const stores = useResolvedStore(source); | |
const monitoredProperties = new Set(); | |
useEffect(() => { | |
// prevent reading values after the initial call. | |
locked = true; | |
const changed = (change) => { | |
if (shouldUpdateForChange(change, monitoredProperties)) { | |
forceUpdate(); | |
} | |
}; | |
addChangeListener(stores, changed); | |
return () => { | |
removeChangeListener(stores, changed); | |
}; | |
}, [stores]); | |
return new Proxy(BLANK_TARGET, { | |
...BASE_PROXY_TRAPS, | |
get(_, propertyName) { | |
if (locked) { | |
throw new Error( | |
"Do not store a reference to this intermediate proxy. Get values as properties and discard." | |
); | |
} | |
monitoredProperties.add(propertyName); | |
return getValueFromStore(stores, propertyName); | |
}, | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment