1<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8" data-next-head=""/><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" data-next-head=""/><meta http-equiv="Content-Language" content="en" data-next-head=""/><meta name="author" content="Paco Coursey" data-next-head=""/><link rel="canonical" href="https://paco.me/writing/hook-getter" data-next-head=""/><link rel="alternate" type="application/rss+xml" title="RSS Feed for Paco Coursey" href="/feed.xml" data-next-head=""/><meta property="og:type" content="website" data-next-head=""/><meta property="twitter:card" content="summary" data-next-head=""/><meta property="twitter:site" content="@pacocoursey" data-next-head=""/><link rel="manifest" href="/site.webmanifest" data-next-head=""/><link rel="icon" href="/favicon.svg" type="image/svg+xml" data-next-head=""/><link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="any" data-next-head=""/><link rel="apple-touch-icon" href="/apple-touch-icon.png" data-next-head=""/><link rel="manifest" href="/site.webmanifest" data-next-head=""/><meta property="og:image" content="https://paco.me/twitter-sm.png" data-next-head=""/><meta property="twitter:image" content="https://paco.me/twitter-sm.png" data-next-head=""/><title data-next-head="">React Hook Getter Pattern</title><meta property="og:title" content="React Hook Getter Pattern" data-next-head=""/><meta name="title" content="React Hook Getter Pattern" data-next-head=""/><meta property="twitter:title" content="React Hook Getter Pattern" data-next-head=""/><meta property="description" content="Build a simple, efficient shared React state hook in a few lines." data-next-head=""/><meta property="og:description" content="Build a simple, efficient shared React state hook in a few lines." data-next-head=""/><meta property="twitter:description" content="Build a simple, efficient shared React state hook in a few lines." data-next-head=""/><meta property="og:url" content="https://paco.me/writing/hook-getter" data-next-head=""/><meta property="twitter:url" content="https://paco.me/writing/hook-getter" data-next-head=""/><link rel="preload" href="/fonts/inter-subset.woff2" as="font" type="font/woff2" crossorigin="anonymous"/><link rel="preload" href="/fonts/sohne-subset-0.woff2" as="font" type="font/woff2" crossorigin="anonymous"/><link rel="preload" href="/fonts/newsreader-subset-0.woff2" as="font" type="font/woff2" crossorigin="anonymous"/><meta name="theme-color" content="#1c1c1c" media="(prefers-color-scheme: dark)"/><meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)"/><link rel="preload" href="/_next/static/chunks/6f8cd1f2e1c78e16.css" as="style"/><link rel="preload" href="/_next/static/chunks/043ac70fdbeb5d40.css" as="style"/><link rel="preload" href="/_next/static/chunks/1902af4af6f089fe.css" as="style"/><link rel="stylesheet" href="/_next/static/chunks/6f8cd1f2e1c78e16.css" data-n-g=""/><link rel="stylesheet" href="/_next/static/chunks/043ac70fdbeb5d40.css" data-n-g=""/><link rel="stylesheet" href="/_next/static/chunks/1902af4af6f089fe.css" data-n-p=""/><noscript data-n-css=""></noscript>
1<script src="/_next/static/chunks/25886f24de13fc19.js" defer=""></script>
1<script src="/_next/static/chunks/1e048dd9659beca6.js" defer=""></script>
1<script src="/_next/static/chunks/518fb8e4bea859d9.js" defer=""></script>
1<script src="/_next/static/chunks/6b733089239eb31f.js" defer=""></script>
1<script src="/_next/static/chunks/86bdcd21889838e6.js" defer=""></script>
1<script src="/_next/static/chunks/turbopack-616a0f24dd09dfa4.js" defer=""></script>
1<script src="/_next/static/chunks/92cf5f8d8c1d03a3.js" defer=""></script>
1<script src="/_next/static/chunks/1ad49a52f8044fc6.js" defer=""></script>
1<script src="/_next/static/chunks/turbopack-da8cb37f79136116.js" defer=""></script>
1<script src="/_next/static/yKgKVe2xTvOCTGUYTncug/_ssgManifest.js" defer=""></script>
1<script src="/_next/static/yKgKVe2xTvOCTGUYTncug/_buildManifest.js" defer=""></script>
1</head><body><div id="__next"><div class="blur" aria-hidden="true"></div><main><div class="main-grid"><nav basics-sidenote="" basics-variant="left" class="meta"><a basics-link="" href="/writing"><div style="--basics-G:1;--basics-A:center;--basics-D:var(--ON)" basics-stack=""><span basics-css-icon="" data-icon="arrowUpLeft" style="color:var(--mono12);width:12px;height:12px" role="img" aria-label="arrowUpLeft icon"></span><span basics-text=""><em>Writing</em></span></div></a></nav><h1 basics-heading="" level="1" basics-text="">React Hook Getter Pattern</h1><time class="layout-module__gvheIG__meta">September 27, 2021</time><article basics-prose=""><p>The data fetching library <a href="https://github.com/vercel/swr">swr</a> uses a clever technique to drastically reduce the number of React re-renders in components that share state. I call it <em>hook getters</em>, and just like <a href="https://github.com/pmndrs/zustand#selecting-multiple-state-slices">state selectors</a>, it stems from this idea: if you never use a certain value, why re-render when that value changes?</p> 2<p>For example:</p> 3<pre basics-codeblock=""><code>const Component = () => { 4 const { data } = useSWR() 5 6 // useSWR also returns `error`, but it's unused here 7 // No need to re-render when `error` changes 8}</code></pre><p>Instead of selector callbacks, SWR uses JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get"><code>getters</code></a> to detect when values are being read, and will only re-render for relevant changes. This trick only applies if your state is shared and object-like â which happens to be a majority of state and context in React!</p> 9<h2 basics-heading="" level="2" basics-text=""><a basics-variant="heading " id="implementation" basics-link="" href="#implementation">Implementation</a></h2><p>I wrote a <a href="https://github.com/pacocoursey/state">collection of concepts</a> that implement this pattern with a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy">Proxy</a>. Hereâs the idea:</p> 10<ul> 11<li>Keep state outside of render lifecycle (could be ref, or global)</li> 12<li>State returned from the hook is actually a proxy</li> 13<li>When proxy <code>get</code> is invoked: track which object key is used, return the value</li> 14<li>When proxy <code>set</code> is invoked: update state, broadcast to all listeners</li> 15<li>In each hook, listen for broadcast but only re-render if the changed key is tracked</li> 16</ul> 17<p>Letâs model our state solution after Zustand, where you create state âstoresâ outside of render:</p> 18<pre basics-codeblock=""><code>const useStore = createStore({ 19 name: 'John', 20 username: 'johndoe', 21}) 22 23function NameDisplay() { 24 const state = useStore() 25 26 // Should only re-render when state.name changes, as username is unused 27 return <input value={state.name} onChange={(e) => (state.name = e.target.value)} /> 28}</code></pre><p>When creating a store, weâll track the state and any listeners. When each hook subscribes to the shared state, we just add it to a set of listeners.</p> 29<pre basics-codeblock=""><code>function createStore(initial = {}) { 30 const state = initial 31 const listeners = new Set() 32 33 function subscribe(listener) { 34 listeners.add(listener) 35 return () => { 36 listeners.delete(listener) 37 } 38 } 39}</code></pre><p>Setting state checks for equality and broadcasts to any listeners if the state changed:</p> 40<pre basics-codeblock=""><code>function setState(key, value) { 41 if (!Object.is(state[key], value)) { 42 state[key] = value 43 listeners.forEach((listener) => listener(state, key)) 44 } 45}</code></pre><p>Finally, creating the store returns a hook to actually use the store. It creates a proxy that updates a <code>tracked</code> ref on any <code>get</code> access, and calls <code>setState</code> for any <code>set</code> access.</p> 46<pre basics-codeblock=""><code>return function useStore() { 47 const rerender = useState()[1] 48 const tracked = useRef({}) 49 const stateRef = useRef(state) 50 51 const proxy = useMemo(() => { 52 stateRef.current = state 53 54 return new Proxy( 55 {}, 56 { 57 get(_, property) { 58 tracked.current[property] = true 59 return stateRef.current[property] 60 }, 61 set(_, property, value) { 62 setState(property, value) 63 return true 64 }, 65 }, 66 ) 67 }, []) 68}</code></pre><p>When the hook mounts, it subscribes to the store and listens for any state broadcasts. It only forces a re-render when a state broadcast modified a key the hook is already tracking.</p> 69<pre basics-codeblock=""><code>
69useEffect(() => { 70 const unsub = subscribe((_, key) => { 71 if (tracked.current[key]) { 72 rerender({}) 73 } 74 }) 75 76 return unsub 77}, [])</code></pre><p>Finally, return the proxy.</p> 78<pre basics-codeblock=""><code>return proxy</code></pre><p>Thatâs about it, a simple shared state system in about 50 lines. Full <a href="https://github.com/pacocoursey/state">code is here</a>.</p> 79</article></div></main><footer><div><span basics-color="lowcontrast" basics-size="small" basics-text="">Pray at the altar of hard work.</span><span basics-variant="tnum" basics-color="lowcontrast" basics-size="small" basics-text="">2025</span><div style="cursor:not-allowed"></div></div></footer></div>
79<script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"md":"\u003cp\u003eThe data fetching library \u003ca href=\"https://github.com/vercel/swr\"\u003eswr\u003c/a\u003e uses a clever technique to drastically reduce the number of React re-renders in components that share state. I call it \u003cem\u003ehook getters\u003c/em\u003e, and just like \u003ca href=\"https://github.com/pmndrs/zustand#selecting-multiple-state-slices\"\u003estate selectors\u003c/a\u003e, it stems from this idea: if you never use a certain value, why re-render when that value changes?\u003c/p\u003e\n\u003cp\u003eFor example:\u003c/p\u003e\n\u003cpre basics-codeblock=\"\"\u003e\u003ccode\u003econst Component = () =\u0026gt; {\n const { data } = useSWR()\n\n // useSWR also returns `error`, but it\u0026#x27;s unused here\n // No need to re-render when `error` changes\n}\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eInstead of selector callbacks, SWR uses JavaScript \u003ca href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get\"\u003e\u003ccode\u003egetters\u003c/code\u003e\u003c/a\u003e to detect when values are being read, and will only re-render for relevant changes. This trick only applies if your state is shared and object-like â which happens to be a majority of state and context in React!\u003c/p\u003e\n\u003ch2 basics-heading=\"\" level=\"2\" basics-text=\"\"\u003e\u003ca basics-variant=\"heading \" id=\"implementation\" basics-link=\"\" href=\"#implementation\"\u003eImplementation\u003c/a\u003e\u003c/h2\u003e\u003cp\u003eI wrote a \u003ca href=\"https://github.com/pacocoursey/state\"\u003ecollection of concepts\u003c/a\u003e that implement this pattern with a \u003ca href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy\"\u003eProxy\u003c/a\u003e. Hereâs the idea:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eKeep state outside of render lifecycle (could be ref, or global)\u003c/li\u003e\n\u003cli\u003eState returned from the hook is actually a proxy\u003c/li\u003e\n\u003cli\u003eWhen proxy \u003ccode\u003eget\u003c/code\u003e is invoked: track which object key is used, return the value\u003c/li\u003e\n\u003cli\u003eWhen proxy \u003ccode\u003eset\u003c/code\u003e is invoked: update state, broadcast to all listeners\u003c/li\u003e\n\u003cli\u003eIn each hook, listen for broadcast but only re-render if the changed key is tracked\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eLetâs model our state solution after Zustand, where you create state âstoresâ outside of render:\u003c/p\u003e\n\u003cpre basics-codeblock=\"\"\u003e\u003ccode\u003econst useStore = createStore({\n name: \u0026#x27;John\u0026#x27;,\n username: \u0026#x27;johndoe\u0026#x27;,\n})\n\nfunction NameDisplay() {\n const state = useStore()\n\n // Should only re-render when state.name changes, as username is unused\n return \u0026lt;input value={state.name} onChange={(e) =\u0026gt; (state.name = e.target.value)} /\u0026gt;\n}\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eWhen creating a store, weâll track the state and any listeners. When each hook subscribes to the shared state, we just add it to a set of listeners.\u003c/p\u003e\n\u003cpre basics-codeblock=\"\"\u003e\u003ccode\u003efunction createStore(initial = {}) {\n const state = initial\n const listeners = new Set()\n\n function subscribe(listener) {\n listeners.add(listener)\n return () =\u0026gt; {\n listeners.delete(listener)\n }\n }\n}\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eSetting state checks for equality and broadcasts to any listeners if the state changed:\u003c/p\u003e\n\u003cpre basics-codeblock=\"\"\u003e\u003ccode\u003efunction setState(key, value) {\n if (!Object.is(state[key], value)) {\n state[key] = value\n listeners.forEach((listener) =\u0026gt; listener(state, key))\n }\n}\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eFinally, creating the store returns a hook to actually use the store. It creates a proxy that updates a \u003ccode\u003etracked\u003c/code\u003e ref on any \u003ccode\u003eget\u003c/code\u003e access, and calls \u003ccode\u003esetState\u003c/code\u003e for any \u003ccode\u003eset\u003c/code\u003e access.\u003c/p\u003e\n\u003cpre basics-codeblock=\"\"\u003e\u003ccode\u003ereturn function useStore() {\n const rerender = useState()[1]\n const tracked = useRef({})\n const stateRef = useRef(state)\n\n const proxy = useMemo(() =\u0026gt; {\n stateRef.current = state\n\n return new Proxy(\n {},\n {\n get(_, property) {\n tracked.current[property] = true\n return stateRef.current[property]\n },\n set(_, property, value) {\n setState(property, value)\n return true\n },\n },\n )\n }, [])\n}\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eWhen the hook mounts, it subscribes to the store and listens for any state broadcasts. It only forces a re-render when a state broadcast modified a key the hook is already tracking.\u003c/p\u003e\n\u003cpre basics-codeblock=\"\"\u003e\u003ccode\u003euseEffect(() =\u0026gt; {\n const unsub = subscribe((_, key) =\u0026gt;
79 {\n if (tracked.current[key]) {\n rerender({})\n }\n })\n\n return unsub\n}, [])\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eFinally, return the proxy.\u003c/p\u003e\n\u003cpre basics-codeblock=\"\"\u003e\u003ccode\u003ereturn proxy\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eThatâs about it, a simple shared state system in about 50 lines. Full \u003ca href=\"https://github.com/pacocoursey/state\"\u003ecode is here\u003c/a\u003e.\u003c/p\u003e\n","frontmatter":{"title":"React Hook Getter Pattern","description":"Build a simple, efficient shared React state hook in a few lines.","slug":"hook-getter","date":"September 27, 2021"}},"__N_SSG":true},"page":"/writing/[post]","query":{"post":"hook-getter"},"buildId":"yKgKVe2xTvOCTGUYTncug","isFallback":false,"gsp":true,"scriptLoader":[]}</script>
79</body></html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.