|
| 1 | +# Optimization |
| 2 | + |
| 3 | +## Library philosophy |
| 4 | + |
| 5 | +ReactUse provides **simple, predictable utility hooks**. We do not add shared caches or cross-component optimizations by default. Optimization is a **conscious, application-level decision**, not something the library imposes on every user. |
| 6 | + |
| 7 | +## Why the library stays simple |
| 8 | + |
| 9 | +> "Premature optimization is the root of all evil" β Donald Knuth |
| 10 | +
|
| 11 | +The same principle applies here as with [memoization](/memoization): optimization should be applied when there is a real need, not by default. |
| 12 | + |
| 13 | +- **Simple hooks are easier to reason about and debug** β one subscription per hook instance, no hidden shared state. |
| 14 | +- **Not every app needs one-listener-per-query** β pushing shared stores into the library would add complexity for everyone, including those who do not need it. |
| 15 | +- **Users who need shared or subscription optimizations** can implement or wrap hooks in their application, or use helpers when appropriate. |
| 16 | + |
| 17 | +## Hooks that can be optimized |
| 18 | + |
| 19 | +The library ships a straightforward `useMediaQuery`: each component that uses it gets its own `matchMedia` subscription and change listener. No shared cache. |
| 20 | + |
| 21 | +**Current implementation:** |
| 22 | + |
| 23 | +```ts |
| 24 | +import { useCallback, useSyncExternalStore } from 'react'; |
| 25 | + |
| 26 | +const getServerSnapshot = () => false; |
| 27 | + |
| 28 | +export const useMediaQuery = (query: string) => { |
| 29 | + const subscribe = useCallback( |
| 30 | + (callback: () => void) => { |
| 31 | + const matchMedia = window.matchMedia(query); |
| 32 | + |
| 33 | + // Each hook call gets its own listener |
| 34 | + matchMedia.addEventListener('change', callback); |
| 35 | + return () => { |
| 36 | + matchMedia.removeEventListener('change', callback); |
| 37 | + }; |
| 38 | + }, |
| 39 | + [query] |
| 40 | + ); |
| 41 | + |
| 42 | + const getSnapshot = () => window.matchMedia(query).matches; |
| 43 | + |
| 44 | + return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); |
| 45 | +}; |
| 46 | +``` |
| 47 | + |
| 48 | +If you measure and find that many components use the same query and you want a single listener per query, you can do that **in your application**. A module-level cache of external stores, one `MediaQueryList` and one `change` listener per unique query. |
| 49 | + |
| 50 | +**Optimized variant:** |
| 51 | + |
| 52 | +```ts |
| 53 | +import { useSyncExternalStore } from 'react'; |
| 54 | + |
| 55 | +const getServerSnapshot = () => false; |
| 56 | + |
| 57 | +interface MediaQueryListExternalStore { |
| 58 | + getSnapshot: () => boolean; |
| 59 | + subscribe: (onStoreChange: () => void) => () => void; |
| 60 | +} |
| 61 | + |
| 62 | +const mediaQueryListExternalStore = new Map<string, MediaQueryListExternalStore>(); |
| 63 | + |
| 64 | +const createMediaQueryExternalStore = (query: string): MediaQueryListExternalStore => { |
| 65 | + const mediaQueryList = window.matchMedia(query); |
| 66 | + const listeners = new Set<() => void>(); |
| 67 | + const onChange = () => listeners.forEach((listener) => listener()); |
| 68 | + |
| 69 | + const store: MediaQueryListExternalStore = { |
| 70 | + subscribe: (onStoreChange) => { |
| 71 | + listeners.add(onStoreChange); |
| 72 | + if (listeners.size === 1) mediaQueryList.addEventListener('change', onChange); |
| 73 | + return () => { |
| 74 | + listeners.delete(onStoreChange); |
| 75 | + if (listeners.size === 0) { |
| 76 | + mediaQueryList.removeEventListener('change', onChange); |
| 77 | + } |
| 78 | + }; |
| 79 | + }, |
| 80 | + getSnapshot: () => mediaQueryList.matches |
| 81 | + }; |
| 82 | + mediaQueryListExternalStore.set(query, store); |
| 83 | + return store; |
| 84 | +}; |
| 85 | + |
| 86 | +const getMediaQueryExternalStore = (query: string) => |
| 87 | + mediaQueryListExternalStore.get(query) ?? createMediaQueryExternalStore(query); |
| 88 | + |
| 89 | +export const useMediaQuery = (query: string) => { |
| 90 | + const store = getMediaQueryExternalStore(query); |
| 91 | + return useSyncExternalStore(store.subscribe, store.getSnapshot, getServerSnapshot); |
| 92 | +}; |
| 93 | +``` |
| 94 | + |
| 95 | +The library keeps the simple version by design; the optimized version is something you can adopt in your codebase if it fits your needs. |
| 96 | + |
| 97 | +## createSharedHook |
| 98 | + |
| 99 | +ReactUse also provides [**createSharedHook**](/functions/hooks/createSharedHook.html): a helper that creates one shared instance of a hook globally. The first subscriber's arguments are used; when the number of subscribers drops to zero, the internal runner unmounts. |
| 100 | + |
| 101 | +```tsx |
| 102 | +import { createSharedHook, useMediaQuery } from '@siberiacancode/reactuse'; |
| 103 | + |
| 104 | +const useSharedMediaQuery = createSharedHook(useMediaQuery); |
| 105 | + |
| 106 | +const First = () => { |
| 107 | + const matches = useSharedMediaQuery('(max-width: 768px)'); |
| 108 | + return ( |
| 109 | + <div> |
| 110 | + This is <code>{matches ? 'mobile' : 'desktop'}</code> screen |
| 111 | + </div> |
| 112 | + ); |
| 113 | +}; |
| 114 | + |
| 115 | +const Second = () => { |
| 116 | + const matches = useSharedMediaQuery('(max-width: 768px)'); |
| 117 | + return ( |
| 118 | + <div> |
| 119 | + This is <code>{matches ? 'mobile' : 'desktop'}</code> screen |
| 120 | + </div> |
| 121 | + ); |
| 122 | +}; |
| 123 | + |
| 124 | +const Demo = () => ( |
| 125 | + <div> |
| 126 | + <First /> |
| 127 | + <Second /> |
| 128 | + </div> |
| 129 | +); |
| 130 | +``` |
| 131 | + |
| 132 | +**Important:** **createSharedHook is experimental.** It mounts an internal component in memory and has limitations: hook order is fixed, and the first subscriber's arguments are used for everyone. For production shared state, we recommend: |
| 133 | + |
| 134 | +- **External stores + useSyncExternalStore** β e.g. the optimized useMediaQuery pattern above, or your own store keyed by query or other arguments. |
| 135 | +- **Dedicated state libraries** β such as Zustand, Reatom, Effector, or similar β especially for complex shared state, as noted in the `createSharedHook` source. |
0 commit comments