Fun with Proxies
— Comments — code, tips, tricks — 3 min read
It was 2020, NextThought had recently dropped support for IE, and I had just shuffled back onto our core platform project, and I had some new ideas
With the advent of functional components and hooks in React, and the apparent never-ending limbo decorators had fallen into, we needed a more ergonomic way to subscribe to changes in our data stores.
Our in-house stores had gone through several iterations for binding to components. First, there were mixins… but then mixins turned out to be harmful. Then we shifted to High-Order-Components (HoC) and decorators to “connect”… but then the decorator's proposal in TC39 went back to the drawing board and derailed that… plus, all these variants required the component to use a class structure.
Oh, and as a lesson learned: we hated the misdirection of where “stuff came from” when trying to maintain these mixins/HoC/decorated components & codes.
Enter our useValue
hook
This was the first iteration and it returned a computed object of desired values given a list of fields in the store as input that it would also use to subscribe to their changes. (hand wave where the store was and how it was initialized) Suffice to say, the components got the value from the store, and if it is updated in the store the components re-rendered.
This was the right direction, but I felt I could make it better… I wanted to make the signature less repetitive. So instead of:
const { foo } = useValue([‘foo’])
I wanted it to be just:
const { foo } = useValue();
However, I didn't want it to subscribe to all field changes, just the ones that were used.
We had recently dropped support for IE, which meant Proxies were on the table! Proxies can execute traps for various object interactions allowing you to intercept them and do something. So if you had a proxy and tried to, say, read a property. You could catch it. Not only that, but you could know the name of the property as well. Here is an example:
let theTarget = {};let myProxy = new Proxy(theTarget, { get(target, propertyName, receiver) { // these asserts are just to help explain what these two arguments are: assert(receiver === myProxy); assert(target === theTarget); console.log('You just tried to get:', propertyName, 'from', receiver, 'this proxy wraps', target); return 'Hi'; },});
console.log('Proxy says:', myProxy.catchPhrase);
The above snippet will output:
You just tried to get: catchPhrase from [object] this proxy wraps [object]Proxy says: Hi
Pretty handy, no? 😄
Here is a rudimentary hook showing the concept:
function useValue() { const store = useContext(StoreContext); //hand waving const monitoredProperties = new Set();
useEffect(() => { const unsubscribe = store.subscribe(monitoredProperties, forceUpdate); return () => unsubscribe(); }, [store]);
return new Proxy( {}, { get(_, propertyName) { monitoredProperties.add(propertyName); return store[propertyName]; }, }, );}
The proxy allows us to collect the properties that were used, and then subscribe to them in the store! This example is not the complete implementation. There are still a few things to resolve but are beyond what I wanted to talk about. You can refer to my gist (use-store-value.js
) that was ripped out of my production implementation if you want to explore this for yourself :)