@pluv/react 0.17.3 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,38 +9,13 @@ import type {
9
9
  WebSocketConnection,
10
10
  } from "@pluv/client";
11
11
  import { MockedRoom } from "@pluv/client";
12
- import type {
13
- AbstractCrdtDoc,
14
- AbstractCrdtDocFactory,
15
- AbstractCrdtType,
16
- InferCrdtStorageJson,
17
- } from "@pluv/crdt";
12
+ import type { AbstractCrdtDoc, AbstractCrdtDocFactory, AbstractCrdtType, InferCrdtStorageJson } from "@pluv/crdt";
18
13
  import { noop } from "@pluv/crdt";
19
- import type {
20
- IOEventMessage,
21
- IOLike,
22
- Id,
23
- InferIOInput,
24
- InferIOOutput,
25
- InputZodLike,
26
- JsonObject,
27
- } from "@pluv/types";
14
+ import type { IOEventMessage, IOLike, Id, InferIOInput, InferIOOutput, InputZodLike, JsonObject } from "@pluv/types";
28
15
  import fastDeepEqual from "fast-deep-equal";
29
16
  import type { Dispatch, FC, ReactNode } from "react";
30
- import {
31
- createContext,
32
- memo,
33
- useCallback,
34
- useContext,
35
- useEffect,
36
- useState,
37
- } from "react";
38
- import {
39
- identity,
40
- shallowArrayEqual,
41
- useRerender,
42
- useSyncExternalStoreWithSelector,
43
- } from "./internal";
17
+ import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from "react";
18
+ import { identity, shallowArrayEqual, useRerender, useSyncExternalStoreWithSelector } from "./internal";
44
19
 
45
20
  export type CreateRoomBundleOptions<
46
21
  TIO extends IOLike,
@@ -59,9 +34,7 @@ type BaseRoomProviderProps<
59
34
  children?: ReactNode;
60
35
  initialStorage?: keyof TStorage extends never ? never : () => TStorage;
61
36
  room: string;
62
- } & (keyof TPresence extends never
63
- ? { initialPresence?: never }
64
- : { initialPresence: TPresence });
37
+ } & (keyof TPresence extends never ? { initialPresence?: never } : { initialPresence: TPresence });
65
38
 
66
39
  export type MockedRoomProviderProps<
67
40
  TIO extends IOLike,
@@ -88,6 +61,19 @@ export type UpdateMyPresenceAction<TPresence extends JsonObject> =
88
61
  | Partial<TPresence>
89
62
  | ((oldPresence: TPresence | null) => Partial<TPresence>);
90
63
 
64
+ export type BroadcastProxy<TIO extends IOLike> = (<TEvent extends keyof InferIOInput<TIO>>(
65
+ event: TEvent,
66
+ data: Id<InferIOInput<TIO>[TEvent]>,
67
+ ) => void) & {
68
+ [event in keyof InferIOInput<TIO>]: (input: Id<InferIOInput<TIO>[event]>) => void;
69
+ };
70
+
71
+ export type EventProxy<TIO extends IOLike> = {
72
+ [event in keyof InferIOOutput<TIO>]: {
73
+ useEvent: (callback: (data: Id<IOEventMessage<TIO, event>>) => void) => void;
74
+ };
75
+ };
76
+
91
77
  export interface CreateRoomBundle<
92
78
  TIO extends IOLike,
93
79
  TPresence extends JsonObject,
@@ -97,11 +83,11 @@ export interface CreateRoomBundle<
97
83
  MockedRoomProvider: FC<MockedRoomProviderProps<TIO, TPresence, TStorage>>;
98
84
  PluvRoomProvider: FC<PluvRoomProviderProps<TIO, TPresence, TStorage>>;
99
85
 
86
+ // proxies
87
+ event: EventProxy<TIO>;
88
+
100
89
  // hooks
101
- useBroadcast: () => <TEvent extends keyof InferIOInput<TIO>>(
102
- event: TEvent,
103
- data: Id<InferIOInput<TIO>[TEvent]>,
104
- ) => void;
90
+ useBroadcast: () => BroadcastProxy<TIO>;
105
91
  useCanRedo: () => boolean;
106
92
  useCanUndo: () => boolean;
107
93
  useConnection: <T extends unknown = WebSocketConnection>(
@@ -116,10 +102,7 @@ export interface CreateRoomBundle<
116
102
  useMyPresence: <T extends unknown = TPresence>(
117
103
  selector?: (myPresence: TPresence) => T,
118
104
  options?: SubscriptionHookOptions<Id<T> | null>,
119
- ) => [
120
- myPresence: Id<T>,
121
- updateMyPresence: Dispatch<UpdateMyPresenceAction<TPresence>>,
122
- ];
105
+ ) => [myPresence: Id<T>, updateMyPresence: Dispatch<UpdateMyPresenceAction<TPresence>>];
123
106
  useMyself: <T extends unknown = UserInfo<TIO, TPresence>>(
124
107
  selector?: (myself: Id<UserInfo<TIO, TPresence>>) => T,
125
108
  options?: SubscriptionHookOptions<Id<T> | null>,
@@ -127,35 +110,25 @@ export interface CreateRoomBundle<
127
110
  useOther: <T extends unknown = UserInfo<TIO, TPresence>>(
128
111
  connectionId: string,
129
112
  selector?: (other: UserInfo<TIO, TPresence>) => T,
130
- options?: SubscriptionHookOptions<Id<T> | null>,
131
- ) => Id<T> | null;
132
- useOthers: <T extends unknown = UserInfo<TIO, TPresence>>(
133
- selector?: (other: readonly Id<UserInfo<TIO, TPresence>>[]) => T[],
134
- options?: SubscriptionHookOptions<readonly Id<T>[]>,
135
- ) => readonly Id<T>[];
113
+ options?: SubscriptionHookOptions<T | null>,
114
+ ) => T | null;
115
+ useOthers: <T extends unknown = readonly UserInfo<TIO, TPresence>[]>(
116
+ selector?: (other: readonly Id<UserInfo<TIO, TPresence>>[]) => T,
117
+ options?: SubscriptionHookOptions<T>,
118
+ ) => T;
136
119
  useRedo: () => () => void;
137
120
  useRoom: () => AbstractRoom<TIO, TPresence, TStorage>;
138
- useStorage: <
139
- TKey extends keyof TStorage,
140
- TData extends unknown = InferCrdtStorageJson<TStorage[TKey]>,
141
- >(
121
+ useStorage: <TKey extends keyof TStorage, TData extends unknown = InferCrdtStorageJson<TStorage[TKey]>>(
142
122
  key: TKey,
143
123
  selector?: (data: InferCrdtStorageJson<TStorage[TKey]>) => TData,
144
124
  options?: SubscriptionHookOptions<TData | null>,
145
125
  ) => [data: TData | null, sharedType: TStorage[TKey] | null];
146
- useTransact: () => (
147
- fn: (storage: TStorage) => void,
148
- origin?: string,
149
- ) => void;
126
+ useTransact: () => (fn: (storage: TStorage) => void, origin?: string) => void;
150
127
  useUndo: () => () => void;
151
128
  }
152
129
 
153
- export type InferRoomStorage<
154
- TRoomBundle extends CreateRoomBundle<any, any, any>,
155
- > =
156
- TRoomBundle extends CreateRoomBundle<any, any, infer IStorage>
157
- ? IStorage
158
- : never;
130
+ export type InferRoomStorage<TRoomBundle extends CreateRoomBundle<any, any, any>> =
131
+ TRoomBundle extends CreateRoomBundle<any, any, infer IStorage> ? IStorage : never;
159
132
 
160
133
  export const createRoomBundle = <
161
134
  TIO extends IOLike,
@@ -165,8 +138,7 @@ export const createRoomBundle = <
165
138
  client: PluvClient<TIO>,
166
139
  options: CreateRoomBundleOptions<TIO, TPresence, TStorage>,
167
140
  ): CreateRoomBundle<TIO, TPresence, TStorage> => {
168
- const _initialStorage = (options.initialStorage ??
169
- noop.doc()) as AbstractCrdtDocFactory<TStorage>;
141
+ const _initialStorage = (options.initialStorage ?? noop.doc()) as AbstractCrdtDocFactory<TStorage>;
170
142
 
171
143
  /**
172
144
  * !HACK
@@ -175,59 +147,36 @@ export const createRoomBundle = <
175
147
  * @author leedavidcs
176
148
  * @date November 11, 2022
177
149
  */
178
- const PluvRoomContext = createContext<
179
- AbstractRoom<TIO, TPresence, TStorage>
180
- >(null as any);
150
+ const PluvRoomContext = createContext<AbstractRoom<TIO, TPresence, TStorage>>(null as any);
181
151
 
182
152
  const MockedRoomContext = createContext<{
183
153
  room: AbstractRoom<TIO, TPresence, TStorage> | null;
184
154
  }>({ room: null });
185
155
 
186
- const MockedRoomProvider = memo<
187
- MockedRoomProviderProps<TIO, TPresence, TStorage>
188
- >((props) => {
189
- const {
190
- children,
191
- events,
192
- initialPresence,
193
- initialStorage,
194
- room: _room,
195
- } = props;
156
+ const MockedRoomProvider = memo<MockedRoomProviderProps<TIO, TPresence, TStorage>>((props) => {
157
+ const { children, events, initialPresence, initialStorage, room: _room } = props;
196
158
 
197
159
  const [room] = useState<MockedRoom<TIO, TPresence, TStorage>>(() => {
198
160
  return new MockedRoom<TIO, TPresence, TStorage>(_room, {
199
161
  events,
200
162
  initialPresence,
201
163
  initialStorage:
202
- typeof initialStorage === "function"
203
- ? _initialStorage.getFactory(initialStorage)
204
- : _initialStorage,
164
+ typeof initialStorage === "function" ? _initialStorage.getFactory(initialStorage) : _initialStorage,
205
165
  presence: options.presence,
206
166
  });
207
167
  });
208
168
 
209
169
  return (
210
170
  <MockedRoomContext.Provider value={{ room }}>
211
- <PluvRoomContext.Provider value={room}>
212
- {children}
213
- </PluvRoomContext.Provider>
171
+ <PluvRoomContext.Provider value={room}>{children}</PluvRoomContext.Provider>
214
172
  </MockedRoomContext.Provider>
215
173
  );
216
174
  });
217
175
 
218
176
  MockedRoomProvider.displayName = "MockedRoomProvider";
219
177
 
220
- const PluvRoomProvider = memo<
221
- PluvRoomProviderProps<TIO, TPresence, TStorage>
222
- >((props) => {
223
- const {
224
- children,
225
- debug,
226
- initialPresence,
227
- initialStorage,
228
- onAuthorizationFail,
229
- room: _room,
230
- } = props;
178
+ const PluvRoomProvider = memo<PluvRoomProviderProps<TIO, TPresence, TStorage>>((props) => {
179
+ const { children, debug, initialPresence, initialStorage, onAuthorizationFail, room: _room } = props;
231
180
 
232
181
  const rerender = useRerender();
233
182
  const { room: mockedRoom } = useContext(MockedRoomContext);
@@ -238,9 +187,7 @@ export const createRoomBundle = <
238
187
  debug,
239
188
  initialPresence,
240
189
  initialStorage:
241
- typeof initialStorage === "function"
242
- ? _initialStorage.getFactory(initialStorage)
243
- : _initialStorage,
190
+ typeof initialStorage === "function" ? _initialStorage.getFactory(initialStorage) : _initialStorage,
244
191
  presence: options.presence,
245
192
  onAuthorizationFail,
246
193
  });
@@ -264,50 +211,42 @@ export const createRoomBundle = <
264
211
  };
265
212
  }, [room]);
266
213
 
267
- return (
268
- <PluvRoomContext.Provider value={mockedRoom ?? room}>
269
- {children}
270
- </PluvRoomContext.Provider>
271
- );
214
+ return <PluvRoomContext.Provider value={mockedRoom ?? room}>{children}</PluvRoomContext.Provider>;
272
215
  });
273
216
 
274
217
  PluvRoomProvider.displayName = "PluvRoomProvider";
275
218
 
276
219
  const useRoom = () => useContext(PluvRoomContext);
277
220
 
278
- const useBroadcast = (): (<TEvent extends keyof InferIOInput<TIO>>(
279
- event: TEvent,
280
- data: Id<InferIOInput<TIO>[TEvent]>,
281
- ) => void) => {
221
+ const useBroadcast = (): BroadcastProxy<TIO> => {
282
222
  const room = useRoom();
283
223
 
284
- return useCallback(
285
- <TEvent extends keyof InferIOInput<TIO>>(
286
- event: TEvent,
287
- data: Id<InferIOInput<TIO>[TEvent]>,
288
- ) => {
224
+ const broadcast = useCallback(
225
+ <TEvent extends keyof InferIOInput<TIO>>(event: TEvent, data: Id<InferIOInput<TIO>[TEvent]>) => {
289
226
  room.broadcast(event, data);
290
227
  },
291
228
  [room],
292
229
  );
230
+
231
+ return useMemo((): BroadcastProxy<TIO> => {
232
+ return new Proxy(broadcast, {
233
+ get(fn, prop) {
234
+ return (data: Id<InferIOInput<TIO>[keyof InferIOInput<TIO>]>): void => {
235
+ return fn(prop as keyof InferIOInput<TIO>, data);
236
+ };
237
+ },
238
+ }) as BroadcastProxy<TIO>;
239
+ }, [broadcast]);
293
240
  };
294
241
 
295
242
  const useCanRedo = (): boolean => {
296
243
  const room = useRoom();
297
244
 
298
- const subscribe = useCallback(
299
- (onStoreChange: () => void) => room.storageRoot(onStoreChange),
300
- [room],
301
- );
245
+ const subscribe = useCallback((onStoreChange: () => void) => room.storageRoot(onStoreChange), [room]);
302
246
 
303
247
  const getSnapshot = useCallback((): boolean => room.canRedo(), [room]);
304
248
 
305
- const canRedo = useSyncExternalStoreWithSelector(
306
- subscribe,
307
- getSnapshot,
308
- getSnapshot,
309
- identity,
310
- );
249
+ const canRedo = useSyncExternalStoreWithSelector(subscribe, getSnapshot, getSnapshot, identity);
311
250
 
312
251
  return canRedo;
313
252
  };
@@ -315,21 +254,13 @@ export const createRoomBundle = <
315
254
  const useCanUndo = (): boolean => {
316
255
  const room = useRoom();
317
256
 
318
- const subscribe = useCallback(
319
- (onStoreChange: () => void) => room.storageRoot(onStoreChange),
320
- [room],
321
- );
257
+ const subscribe = useCallback((onStoreChange: () => void) => room.storageRoot(onStoreChange), [room]);
322
258
 
323
259
  const getSnapshot = useCallback((): boolean => room.canUndo(), [room]);
324
260
 
325
- const canRedo = useSyncExternalStoreWithSelector(
326
- subscribe,
327
- getSnapshot,
328
- getSnapshot,
329
- identity,
330
- );
261
+ const canUndo = useSyncExternalStoreWithSelector(subscribe, getSnapshot, getSnapshot, identity);
331
262
 
332
- return canRedo;
263
+ return canUndo;
333
264
  };
334
265
 
335
266
  const useConnection = <T extends unknown = WebSocketConnection>(
@@ -347,10 +278,7 @@ export const createRoomBundle = <
347
278
 
348
279
  const getSnapshot = room.getConnection;
349
280
 
350
- const _selector = useCallback(
351
- (snapshot: WebSocketConnection) => selector(snapshot) as Id<T>,
352
- [selector],
353
- );
281
+ const _selector = useCallback((snapshot: WebSocketConnection) => selector(snapshot) as Id<T>, [selector]);
354
282
 
355
283
  return useSyncExternalStoreWithSelector(
356
284
  subscribe,
@@ -382,6 +310,21 @@ export const createRoomBundle = <
382
310
  }, [callback, room, type]);
383
311
  };
384
312
 
313
+ const event = new Proxy(
314
+ {},
315
+ {
316
+ get(_, prop) {
317
+ const useProxyEvent = (
318
+ callback: (data: Id<IOEventMessage<TIO, keyof InferIOOutput<TIO>>>) => void,
319
+ ): void => {
320
+ return useEvent(prop as keyof InferIOOutput<TIO>, callback);
321
+ };
322
+
323
+ return { useEvent: useProxyEvent };
324
+ },
325
+ },
326
+ ) as EventProxy<TIO>;
327
+
385
328
  const useMyPresence = <T extends unknown = TPresence>(
386
329
  selector = identity as (myPresence: TPresence) => T,
387
330
  options?: SubscriptionHookOptions<Id<T> | null>,
@@ -397,10 +340,7 @@ export const createRoomBundle = <
397
340
 
398
341
  const getSnapshot = room.getMyPresence;
399
342
 
400
- const _selector = useCallback(
401
- (snapshot: TPresence) => selector(snapshot) as Id<T>,
402
- [selector],
403
- );
343
+ const _selector = useCallback((snapshot: TPresence) => selector(snapshot) as Id<T>, [selector]);
404
344
 
405
345
  const myPresence = useSyncExternalStoreWithSelector(
406
346
  subscribe,
@@ -410,20 +350,7 @@ export const createRoomBundle = <
410
350
  options?.isEqual ?? fastDeepEqual,
411
351
  );
412
352
 
413
- const updateMyPresence: Dispatch<UpdateMyPresenceAction<TPresence>> =
414
- useCallback(
415
- (value: UpdateMyPresenceAction<TPresence>): void => {
416
- const presence =
417
- typeof value === "function"
418
- ? value(room.getMyPresence())
419
- : value;
420
-
421
- room.updateMyPresence(presence);
422
- },
423
- [room],
424
- );
425
-
426
- return [myPresence, updateMyPresence];
353
+ return [myPresence, room.updateMyPresence];
427
354
  };
428
355
 
429
356
  const useMyself = <T extends unknown = UserInfo<TIO, TPresence>>(
@@ -460,8 +387,8 @@ export const createRoomBundle = <
460
387
  const useOther = <T extends unknown = UserInfo<TIO, TPresence>>(
461
388
  connectionId: string,
462
389
  selector = identity as (other: UserInfo<TIO, TPresence>) => T,
463
- options?: SubscriptionHookOptions<Id<T> | null>,
464
- ): Id<T> | null => {
390
+ options?: SubscriptionHookOptions<T | null>,
391
+ ): T | null => {
465
392
  const room = useRoom();
466
393
 
467
394
  const subscribe = useCallback(
@@ -471,14 +398,11 @@ export const createRoomBundle = <
471
398
  [room, connectionId],
472
399
  );
473
400
 
474
- const getSnapshot = useCallback(
475
- () => room.getOther(connectionId),
476
- [room, connectionId],
477
- );
401
+ const getSnapshot = useCallback(() => room.getOther(connectionId), [room, connectionId]);
478
402
 
479
403
  const _selector = useCallback(
480
404
  (snapshot: Id<UserInfo<TIO, TPresence>> | null) => {
481
- return !snapshot ? null : (selector(snapshot) as Id<T>);
405
+ return !snapshot ? null : (selector(snapshot) as T);
482
406
  },
483
407
  [selector],
484
408
  );
@@ -492,12 +416,10 @@ export const createRoomBundle = <
492
416
  );
493
417
  };
494
418
 
495
- const useOthers = <T extends unknown = UserInfo<TIO, TPresence>>(
496
- selector = identity as (
497
- other: readonly Id<UserInfo<TIO, TPresence>>[],
498
- ) => T[],
499
- options?: SubscriptionHookOptions<readonly Id<T>[]>,
500
- ): readonly Id<T>[] => {
419
+ const useOthers = <T extends unknown = readonly UserInfo<TIO, TPresence>[]>(
420
+ selector = identity as (other: readonly Id<UserInfo<TIO, TPresence>>[]) => T,
421
+ options?: SubscriptionHookOptions<T>,
422
+ ): T => {
501
423
  const room = useRoom();
502
424
 
503
425
  const subscribe = useCallback(
@@ -513,10 +435,23 @@ export const createRoomBundle = <
513
435
  subscribe,
514
436
  getSnapshot,
515
437
  getSnapshot,
516
- selector as (
517
- other: readonly Id<UserInfo<TIO, TPresence>>[],
518
- ) => Id<T>[],
519
- options?.isEqual ?? shallowArrayEqual,
438
+ selector as (other: readonly Id<UserInfo<TIO, TPresence>>[]) => T,
439
+ options?.isEqual ??
440
+ ((a, b) => {
441
+ /**
442
+ * !HACK
443
+ * @description Assume the return type will always be an array when this occurs
444
+ * and do a shallow comparison instead like how standard React dependency
445
+ * arrays work. This is for performance's sake and may lead to bugs if the
446
+ * selector doesn't always return an array
447
+ * @date June 22, 2024
448
+ */
449
+ if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
450
+ return shallowArrayEqual(a, b);
451
+ }
452
+
453
+ return fastDeepEqual(a, b);
454
+ }),
520
455
  );
521
456
  };
522
457
 
@@ -526,14 +461,9 @@ export const createRoomBundle = <
526
461
  return room.redo;
527
462
  };
528
463
 
529
- const useStorage = <
530
- TKey extends keyof TStorage,
531
- TData extends unknown = InferCrdtStorageJson<TStorage[TKey]>,
532
- >(
464
+ const useStorage = <TKey extends keyof TStorage, TData extends unknown = InferCrdtStorageJson<TStorage[TKey]>>(
533
465
  key: TKey,
534
- selector = identity as (
535
- data: InferCrdtStorageJson<TStorage[TKey]>,
536
- ) => TData,
466
+ selector = identity as (data: InferCrdtStorageJson<TStorage[TKey]>) => TData,
537
467
  options?: SubscriptionHookOptions<TData | null>,
538
468
  ): [data: TData | null, sharedType: TStorage[TKey] | null] => {
539
469
  const room = useRoom();
@@ -543,14 +473,9 @@ export const createRoomBundle = <
543
473
  rerender();
544
474
  });
545
475
 
546
- const subscribe = useCallback(
547
- (onStoreChange: () => void) => room.storage(key, onStoreChange),
548
- [key, room],
549
- );
476
+ const subscribe = useCallback((onStoreChange: () => void) => room.storage(key, onStoreChange), [key, room]);
550
477
 
551
- const getSnapshot = useCallback((): InferCrdtStorageJson<
552
- TStorage[TKey]
553
- > | null => {
478
+ const getSnapshot = useCallback((): InferCrdtStorageJson<TStorage[TKey]> | null => {
554
479
  return room.getStorage(key)?.toJson();
555
480
  }, [key, room]);
556
481
 
@@ -591,6 +516,9 @@ export const createRoomBundle = <
591
516
  MockedRoomProvider,
592
517
  PluvRoomProvider,
593
518
 
519
+ // proxies
520
+ event,
521
+
594
522
  // hooks
595
523
  useBroadcast,
596
524
  useCanRedo,
package/src/index.ts CHANGED
@@ -1,11 +1,6 @@
1
1
  export { ConnectionState, createClient } from "@pluv/client";
2
2
  export type { AbstractCrdtType, InferCrdtStorageJson } from "@pluv/crdt";
3
- export type {
4
- BaseUser,
5
- EventMessage,
6
- EventRecord,
7
- IOEventMessage,
8
- } from "@pluv/types";
3
+ export type { BaseUser, EventMessage, EventRecord, IOEventMessage } from "@pluv/types";
9
4
  export { createBundle } from "./createBundle";
10
5
  export type { CreateBundle, PluvProviderProps } from "./createBundle";
11
6
  export type {
@@ -1,3 +1,3 @@
1
+ export { useNoSsr } from "./useNoSsr";
1
2
  export { useRerender } from "./useRerender";
2
- export { useSyncExternalStore } from "./useSyncExternalStore";
3
3
  export { useSyncExternalStoreWithSelector } from "./useSyncExternalStoreWithSelector";
@@ -0,0 +1,18 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+
3
+ export type NoSsrFunction = <T = null>(value: T | (() => T), fallback?: T) => T;
4
+
5
+ export const useNoSsr = (): NoSsrFunction => {
6
+ const [didMount, setDidMount] = useState<boolean>(false);
7
+
8
+ useEffect(() => setDidMount(typeof window !== "undefined"), []);
9
+
10
+ const noSsr = useCallback(
11
+ <T = null>(value: T | (() => T), fallback: T = null as T): T => {
12
+ return didMount ? (typeof value !== "function" ? value : (value as () => T)()) : fallback;
13
+ },
14
+ [didMount],
15
+ );
16
+
17
+ return noSsr;
18
+ };
@@ -1 +1,44 @@
1
- export { useSyncExternalStoreWithSelector } from "use-sync-external-store/with-selector.js";
1
+ import { useRef, useSyncExternalStore } from "react";
2
+ import { shallowEqual } from "../utils";
3
+ import { useNoSsr } from "./useNoSsr";
4
+
5
+ export const useSyncExternalStoreWithSelector = <TSnapshot, TSelection>(
6
+ subscribe: (onStoreChange: () => void) => () => void,
7
+ getClientSnapshot: () => TSnapshot,
8
+ getServerSnapshot: undefined | null | (() => TSnapshot),
9
+ selector: (snapshot: TSnapshot) => TSelection,
10
+ isEqual?: (a: TSelection, b: TSelection) => boolean,
11
+ ): TSelection => {
12
+ const noSsr = useNoSsr();
13
+ const cacheRef = useRef<TSelection | null>(null);
14
+
15
+ const _getServerSnapshot = getServerSnapshot
16
+ ? () => {
17
+ const data = getServerSnapshot();
18
+
19
+ return selector(data);
20
+ }
21
+ : undefined;
22
+
23
+ const _getClientSnapshot = () => {
24
+ const data = getClientSnapshot();
25
+ const snapshot = selector(data);
26
+ const compare = isEqual ?? shallowEqual;
27
+ const cached = cacheRef.current;
28
+
29
+ const isUnchanged = cached === null ? snapshot === null : compare(cached, snapshot);
30
+
31
+ /**
32
+ * !HACK
33
+ * @description Memoize only the last snapshot so that we can only rerender on changes
34
+ * @date June 22, 2024
35
+ */
36
+ cacheRef.current = isUnchanged ? cached ?? snapshot : snapshot;
37
+
38
+ return cacheRef.current;
39
+ };
40
+
41
+ const result = useSyncExternalStore(subscribe, _getClientSnapshot, _getServerSnapshot);
42
+
43
+ return typeof window !== "undefined" ? noSsr(result, _getServerSnapshot?.()) : result;
44
+ };
@@ -1,6 +1,2 @@
1
- export {
2
- useRerender,
3
- useSyncExternalStore,
4
- useSyncExternalStoreWithSelector,
5
- } from "./hooks";
1
+ export { useRerender, useSyncExternalStore, useSyncExternalStoreWithSelector } from "./hooks";
6
2
  export { identity, shallowArrayEqual } from "./utils";
@@ -1,2 +1,3 @@
1
1
  export { identity } from "./identity";
2
2
  export { shallowArrayEqual } from "./shallowArrayEqual";
3
+ export { shallowEqual } from "./shallowEqual";
@@ -1,6 +1,3 @@
1
- export const shallowArrayEqual = <T extends unknown>(
2
- a: readonly T[] | T[],
3
- b: readonly T[] | T[]
4
- ): boolean => {
1
+ export const shallowArrayEqual = <T extends unknown>(a: readonly T[] | T[], b: readonly T[] | T[]): boolean => {
5
2
  return a.length === b.length && a.every((item, i) => item === b[i]);
6
3
  };
@@ -0,0 +1 @@
1
+ export const shallowEqual = (a: any, b: any): boolean => a === b;
package/tsconfig.json CHANGED
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "extends": "@pluv/tsconfig/react-library.json",
3
3
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
4
- "exclude": ["node_modules"]
4
+ "exclude": ["node_modules"],
5
+ "compilerOptions": {
6
+ "lib": ["dom", "esnext"]
7
+ }
5
8
  }
@@ -1,15 +0,0 @@
1
- import { useSyncExternalStoreWithSelector } from "./useSyncExternalStoreWithSelector";
2
- import { identity } from "../utils";
3
-
4
- export const useSyncExternalStore = <TSnapshot extends unknown>(
5
- subscribe: (onStoreChange: () => void) => () => void,
6
- getSnapshot: () => TSnapshot,
7
- getServerSnapshot: undefined | null | (() => TSnapshot)
8
- ) => {
9
- return useSyncExternalStoreWithSelector(
10
- subscribe,
11
- getSnapshot,
12
- getServerSnapshot,
13
- identity
14
- );
15
- };