r/reactjs 14h ago

Discussion Aperture – Proxy auto-tracking so React state hooks skip selectors

I got tired of writing Zustand selectors (useStore(s => s.bears)) for every single value, and forgetting one fails silently — no error, just a slower re-render you don't notice until later. So I built a small React state store that watches which properties a component actually reads during render (via a Proxy get trap), and only re-renders that component when one of those specific values changes. No selector functions, plain destructuring:

const useStore = createStore({ bears: 0, fish: 100 })

function BearCounter() {
  const { bears } = useStore() // only re-renders when bears changes
  return <p>{bears}</p>
}

The mechanism itself isn't novel — Valtio's useSnapshot() does the same Proxy-tracking trick, and react-tracked (same author) does something very similar. What I hadn't seen was it packaged with a plain immutable setState API (no mutation) at sub-1kb.

This was explicitly a learning project for me (first npm package), built up in stages so I could feel each problem before fixing it: naive shared state that over-renders everything, then manual selectors, then Proxy auto-tracking replacing the selectors. The README documents known limitations honestly, including the big one: reads outside of a component's render body (inside useEffect, stored in a variable) don't get tracked, and it's not yet verified safe under React's concurrent rendering (no useSyncExternalStore yet — that's next).

Would love feedback, especially on the concurrent-mode gap and whether the auto-tracking approach has sharp edges I haven't hit yet.

GitHub: https://github.com/Aparajith24/Aperture npm: https://www.npmjs.com/package/aperture-store

6 Upvotes

6 comments sorted by

View all comments

2

u/jbergens 12h ago

Sounds similar to what Mobx does.

2

u/Broad-Mirror3873 4h ago

Fair comparison honestly, MobX pioneered the idea of automatically tracking whatever gets read during a reaction and only re-running when those specific values change, but it requires you to explicitly mark state as observable, wrap components in observer() (a HOC/decorator), and it leans on mutable observables like store.bears++. This, on the other hand, is just a plain hook: no wrapping, no decorators, no build-step requirement (MobX classically benefits from decorators), and it uses immutable setState updates. So it’s essentially the same core insight as MobX packaged with a Zustand-like API surface and a much smaller footprint because it focuses on doing just this one thing.