|
1 | | -import { useCallback, useSyncExternalStore } from 'react'; |
| 1 | +import { useCallback, useDebugValue } from 'react'; |
| 2 | +import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'; |
2 | 3 | import BrownieModule from './NativeBrownieModule'; |
3 | 4 |
|
4 | 5 | /** |
@@ -95,23 +96,129 @@ export function setState<K extends keyof BrownieStores>( |
95 | 96 | } |
96 | 97 | } |
97 | 98 |
|
| 99 | +const identity = <T>(x: T): T => x; |
| 100 | + |
| 101 | +const isIterable = (obj: object): obj is Iterable<unknown> => |
| 102 | + Symbol.iterator in obj; |
| 103 | + |
| 104 | +const hasIterableEntries = ( |
| 105 | + value: Iterable<unknown> |
| 106 | +): value is Iterable<unknown> & { entries(): Iterable<[unknown, unknown]> } => |
| 107 | + 'entries' in value; |
| 108 | + |
| 109 | +const compareEntries = ( |
| 110 | + valueA: { entries(): Iterable<[unknown, unknown]> }, |
| 111 | + valueB: { entries(): Iterable<[unknown, unknown]> } |
| 112 | +) => { |
| 113 | + const mapA = valueA instanceof Map ? valueA : new Map(valueA.entries()); |
| 114 | + const mapB = valueB instanceof Map ? valueB : new Map(valueB.entries()); |
| 115 | + if (mapA.size !== mapB.size) { |
| 116 | + return false; |
| 117 | + } |
| 118 | + for (const [key, value] of mapA) { |
| 119 | + if (!mapB.has(key) || !Object.is(value, mapB.get(key))) { |
| 120 | + return false; |
| 121 | + } |
| 122 | + } |
| 123 | + return true; |
| 124 | +}; |
| 125 | + |
| 126 | +const compareIterables = ( |
| 127 | + valueA: Iterable<unknown>, |
| 128 | + valueB: Iterable<unknown> |
| 129 | +) => { |
| 130 | + const iteratorA = valueA[Symbol.iterator](); |
| 131 | + const iteratorB = valueB[Symbol.iterator](); |
| 132 | + let nextA = iteratorA.next(); |
| 133 | + let nextB = iteratorB.next(); |
| 134 | + while (!nextA.done && !nextB.done) { |
| 135 | + if (!Object.is(nextA.value, nextB.value)) { |
| 136 | + return false; |
| 137 | + } |
| 138 | + nextA = iteratorA.next(); |
| 139 | + nextB = iteratorB.next(); |
| 140 | + } |
| 141 | + return !!nextA.done && !!nextB.done; |
| 142 | +}; |
| 143 | + |
| 144 | +/** |
| 145 | + * Shallow equality comparison for use with useBrownieStore selector. |
| 146 | + * Compares objects by their top-level properties. |
| 147 | + */ |
| 148 | +export function shallow<T>(valueA: T, valueB: T): boolean { |
| 149 | + if (Object.is(valueA, valueB)) { |
| 150 | + return true; |
| 151 | + } |
| 152 | + if ( |
| 153 | + typeof valueA !== 'object' || |
| 154 | + valueA === null || |
| 155 | + typeof valueB !== 'object' || |
| 156 | + valueB === null |
| 157 | + ) { |
| 158 | + return false; |
| 159 | + } |
| 160 | + if (Object.getPrototypeOf(valueA) !== Object.getPrototypeOf(valueB)) { |
| 161 | + return false; |
| 162 | + } |
| 163 | + if (isIterable(valueA) && isIterable(valueB)) { |
| 164 | + if (hasIterableEntries(valueA) && hasIterableEntries(valueB)) { |
| 165 | + return compareEntries(valueA, valueB); |
| 166 | + } |
| 167 | + return compareIterables(valueA, valueB); |
| 168 | + } |
| 169 | + return compareEntries( |
| 170 | + { entries: () => Object.entries(valueA) }, |
| 171 | + { entries: () => Object.entries(valueB) } |
| 172 | + ); |
| 173 | +} |
| 174 | + |
98 | 175 | /** |
99 | | - * React hook for subscribing to a native store. |
| 176 | + * React hook for subscribing to a native store with optional selector. |
100 | 177 | * @param key Store key registered in StoreManager |
101 | 178 | * @returns Tuple of [state, setState] for the store |
102 | 179 | */ |
103 | 180 | export function useBrownieStore<K extends keyof BrownieStores>( |
104 | 181 | key: K |
105 | | -): [BrownieStores[K], (action: SetStateAction<BrownieStores[K]>) => void] { |
| 182 | +): [BrownieStores[K], (action: SetStateAction<BrownieStores[K]>) => void]; |
| 183 | + |
| 184 | +/** |
| 185 | + * React hook for subscribing to a native store with selector. |
| 186 | + * @param key Store key registered in StoreManager |
| 187 | + * @param selector Function to select a slice of state |
| 188 | + * @param equalityFn Optional equality function for comparing selected values |
| 189 | + * @returns Tuple of [selectedState, setState] for the store |
| 190 | + */ |
| 191 | +export function useBrownieStore<K extends keyof BrownieStores, U>( |
| 192 | + key: K, |
| 193 | + selector: (state: BrownieStores[K]) => U, |
| 194 | + equalityFn?: (a: U, b: U) => boolean |
| 195 | +): [U, (action: SetStateAction<BrownieStores[K]>) => void]; |
| 196 | + |
| 197 | +export function useBrownieStore<K extends keyof BrownieStores, U>( |
| 198 | + key: K, |
| 199 | + selector?: (state: BrownieStores[K]) => U, |
| 200 | + equalityFn?: (a: U, b: U) => boolean |
| 201 | +): [U | BrownieStores[K], (action: SetStateAction<BrownieStores[K]>) => void] { |
106 | 202 | const sub = useCallback( |
107 | 203 | (listener: () => void) => subscribe(key, listener), |
108 | 204 | [key] |
109 | 205 | ); |
110 | 206 | const snap = useCallback(() => getSnapshot(key), [key]); |
111 | | - const state = useSyncExternalStore(sub, snap, snap); |
| 207 | + |
| 208 | + const slice = useSyncExternalStoreWithSelector( |
| 209 | + sub, |
| 210 | + snap, |
| 211 | + snap, |
| 212 | + selector ?? (identity as (state: BrownieStores[K]) => U), |
| 213 | + equalityFn |
| 214 | + ); |
| 215 | + |
| 216 | + useDebugValue(slice); |
| 217 | + |
112 | 218 | const boundSetState = useCallback( |
113 | 219 | (action: SetStateAction<BrownieStores[K]>) => setState(key, action), |
114 | 220 | [key] |
115 | 221 | ); |
116 | | - return [state, boundSetState]; |
| 222 | + |
| 223 | + return [slice, boundSetState]; |
117 | 224 | } |
0 commit comments