@liveblocks/react 0.18.0-beta2 → 0.18.0-beta4

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.
@@ -1 +1 @@
1
- b78a4b6045d3aab4fd9a5e4686ccac3b534923fe
1
+ ff5604e48f99b968e9dae639dfc5bace0e57087c
package/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
- import * as React from 'react';
2
1
  import { ReactElement, ReactNode } from 'react';
3
- import { JsonObject, LsonObject, LiveObject, BaseUserMeta, Json, Client, Room, BroadcastOptions, History, Others, User } from '@liveblocks/client';
2
+ import { JsonObject, LsonObject, BaseUserMeta, LiveObject, User, Others, Json, Room, BroadcastOptions, History, Client } from '@liveblocks/client';
4
3
  export { Json, JsonObject, shallow } from '@liveblocks/client';
5
4
  import { ToImmutable, Resolve, RoomInitializers } from '@liveblocks/client/internal';
6
5
 
@@ -27,25 +26,32 @@ declare type Props = {
27
26
  */
28
27
  declare function ClientSideSuspense(props: Props): ReactElement;
29
28
 
29
+ declare type RoomProviderProps<TPresence extends JsonObject, TStorage extends LsonObject> = Resolve<{
30
+ /**
31
+ * The id of the room you want to connect to
32
+ */
33
+ id: string;
34
+ children: React.ReactNode;
35
+ } & RoomInitializers<TPresence, TStorage>>;
30
36
  /**
31
37
  * For any function type, returns a similar function type, but without the
32
38
  * first argument.
33
39
  */
34
40
  declare type OmitFirstArg<F> = F extends (first: any, ...rest: infer A) => infer R ? (...args: A) => R : never;
35
- declare type MutationContext<TPresence extends JsonObject, TStorage extends LsonObject> = {
36
- root: LiveObject<TStorage>;
41
+ declare type MutationContext<TPresence extends JsonObject, TStorage extends LsonObject, TUserMeta extends BaseUserMeta> = {
42
+ storage: LiveObject<TStorage>;
43
+ self: User<TPresence, TUserMeta>;
44
+ others: Others<TPresence, TUserMeta>;
37
45
  setMyPresence: (patch: Partial<TPresence>, options?: {
38
46
  addToHistory: boolean;
39
47
  }) => void;
40
48
  };
41
- declare type RoomProviderProps<TPresence extends JsonObject, TStorage extends LsonObject> = Resolve<{
49
+ declare type RoomContextBundle<TPresence extends JsonObject, TStorage extends LsonObject, TUserMeta extends BaseUserMeta, TRoomEvent extends Json> = {
42
50
  /**
43
- * The id of the room you want to connect to
51
+ * You normally don't need to directly interact with the RoomContext, but
52
+ * it can be necessary if you're building an advanced app where you need to
53
+ * set up a context bridge between two React renderers.
44
54
  */
45
- id: string;
46
- children: React.ReactNode;
47
- } & RoomInitializers<TPresence, TStorage>>;
48
- declare type RoomContextBundle<TPresence extends JsonObject, TStorage extends LsonObject, TUserMeta extends BaseUserMeta, TRoomEvent extends Json> = {
49
55
  RoomContext: React.Context<Room<TPresence, TStorage, TUserMeta, TRoomEvent> | null>;
50
56
  /**
51
57
  * Makes a Room available in the component hierarchy below.
@@ -53,6 +59,11 @@ declare type RoomContextBundle<TPresence extends JsonObject, TStorage extends Ls
53
59
  * That means that you can't have 2 RoomProvider with the same room id in your react tree.
54
60
  */
55
61
  RoomProvider(props: RoomProviderProps<TPresence, TStorage>): JSX.Element;
62
+ /**
63
+ * Returns the Room of the nearest RoomProvider above in the React component
64
+ * tree.
65
+ */
66
+ useRoom(): Room<TPresence, TStorage, TUserMeta, TRoomEvent>;
56
67
  /**
57
68
  * Returns a function that batches modifications made during the given function.
58
69
  * All the modifications are sent to other clients in a single message.
@@ -147,6 +158,14 @@ declare type RoomContextBundle<TPresence extends JsonObject, TStorage extends Ls
147
158
  * const object = useObject("obj");
148
159
  */
149
160
  useObject<TKey extends Extract<keyof TStorage, string>>(key: TKey): TStorage[TKey] | null;
161
+ /**
162
+ * Returns the mutable (!) Storage root. This hook exists for
163
+ * backward-compatible reasons.
164
+ *
165
+ * @example
166
+ * const [root] = useStorageRoot();
167
+ */
168
+ useStorageRoot(): [root: LiveObject<TStorage> | null];
150
169
  /**
151
170
  * Returns your entire Liveblocks Storage as an immutable data structure.
152
171
  *
@@ -174,6 +193,38 @@ declare type RoomContextBundle<TPresence extends JsonObject, TStorage extends Ls
174
193
  * those cases, you'll probably want to use a `shallow` comparison check.
175
194
  */
176
195
  useStorage<T>(selector: (root: ToImmutable<TStorage>) => T, isEqual?: (prev: T, curr: T) => boolean): T | null;
196
+ /**
197
+ * Gets the current user once it is connected to the room.
198
+ *
199
+ * @example
200
+ * const me = useSelf();
201
+ * const { x, y } = me.presence.cursor;
202
+ */
203
+ useSelf(): User<TPresence, TUserMeta> | null;
204
+ /**
205
+ * Extract arbitrary data based on the current user.
206
+ *
207
+ * The selector function will get re-evaluated any time your presence data
208
+ * changes.
209
+ *
210
+ * The component that uses this hook will automatically re-render if your
211
+ * selector function returns a different value from its previous run.
212
+ *
213
+ * By default `useSelf()` uses strict `===` to check for equality. Take extra
214
+ * care when returning a computed object or list, for example when you return
215
+ * the result of a .map() or .filter() call from the selector. In those
216
+ * cases, you'll probably want to use a `shallow` comparison check.
217
+ *
218
+ * Will return `null` while Liveblocks isn't connected to a room yet.
219
+ *
220
+ * @example
221
+ * const cursor = useSelf(me => me.presence.cursor);
222
+ * if (cursor !== null) {
223
+ * const { x, y } = cursor;
224
+ * }
225
+ *
226
+ */
227
+ useSelf<T>(selector: (me: User<TPresence, TUserMeta>) => T, isEqual?: (prev: T, curr: T) => boolean): T | null;
177
228
  /**
178
229
  * Returns the presence of the current user of the current room, and a function to update it.
179
230
  * It is different from the setState function returned by the useState hook from React.
@@ -235,53 +286,49 @@ declare type RoomContextBundle<TPresence extends JsonObject, TStorage extends Ls
235
286
  */
236
287
  useOthers<T>(selector: (others: Others<TPresence, TUserMeta>) => T, isEqual?: (prev: T, curr: T) => boolean): T;
237
288
  /**
238
- * Related to useOthers(), but optimized for selecting only "subsets" of
239
- * others. This is useful for performance reasons in particular, because
240
- * selecting only a subset of users also means limiting the number of
241
- * re-renders that will be triggered.
289
+ * Returns an array of connection IDs. This matches the values you'll get by
290
+ * using the `useOthers()` hook.
291
+ *
292
+ * Roughly equivalent to:
293
+ * useOthers((others) => others.map(other => other.connectionId), shallow)
242
294
  *
243
- * Note that there are two ways to use this hook, and depending on how you
244
- * call it, the return value will be slightly different.
295
+ * This is useful in particular to implement efficiently rendering components
296
+ * for each user in the room, e.g. cursors.
245
297
  *
246
298
  * @example
247
- * const ids = useOtherIds();
248
- * // ^^^ number[]
299
+ * const ids = useOthersConnectionIds();
300
+ * // [2, 4, 7]
249
301
  */
250
- useOtherIds(): readonly number[];
302
+ useOthersConnectionIds(): readonly number[];
251
303
  /**
252
304
  * Related to useOthers(), but optimized for selecting only "subsets" of
253
305
  * others. This is useful for performance reasons in particular, because
254
306
  * selecting only a subset of users also means limiting the number of
255
307
  * re-renders that will be triggered.
256
308
  *
257
- * Note that there are two ways to use this hook, and depending on how you
258
- * call it, the return value will be slightly different.
259
- *
260
309
  * @example
261
- * const avatars = useOtherIds(user => user.info.avatar);
310
+ * const avatars = useOthersMapped(user => user.info.avatar);
262
311
  * // ^^^^^^^
263
312
  * // { connectionId: number; data: string }[]
264
313
  *
265
- * The selector function you pass to useOtherIds() is called an "item
314
+ * The selector function you pass to useOthersMapped() is called an "item
266
315
  * selector", and operates on a single user at a time. If you provide an
267
- * (optional) comparison function, it will also work on the _item_ level.
316
+ * (optional) "item comparison" function, it will be used to compare each
317
+ * item pairwise.
268
318
  *
269
319
  * For example, to select multiple properties:
270
320
  *
271
321
  * @example
272
- * const avatarsAndCursors = useOtherIds(
322
+ * const avatarsAndCursors = useOthersMapped(
273
323
  * user => [u.info.avatar, u.presence.cursor],
274
- * shallow, // ❗️
324
+ * shallow, // 👈
275
325
  * );
276
326
  */
277
- useOtherIds<T>(itemSelector: (other: User<TPresence, TUserMeta>) => T, isEqual?: (prev: T, curr: T) => boolean): readonly {
278
- readonly connectionId: number;
279
- readonly data: T;
280
- }[];
327
+ useOthersMapped<T>(itemSelector: (other: User<TPresence, TUserMeta>) => T, itemIsEqual?: (prev: T, curr: T) => boolean): ReadonlyArray<readonly [connectionId: number, data: T]>;
281
328
  /**
282
- * Given a connection ID (as obtained by using `useOtherIds()`), you can call
283
- * this selector deep down in your component stack to only have the component
284
- * re-render if properties for this particular connection change.
329
+ * Given a connection ID (as obtained by using `useOthersConnectionIds`), you can
330
+ * call this selector deep down in your component stack to only have the
331
+ * component re-render if properties for this particular user change.
285
332
  *
286
333
  * @example
287
334
  * // Returns full user and re-renders whenever anything on the user changes
@@ -289,9 +336,9 @@ declare type RoomContextBundle<TPresence extends JsonObject, TStorage extends Ls
289
336
  */
290
337
  useOther(connectionId: number): User<TPresence, TUserMeta>;
291
338
  /**
292
- * Given a connection ID (as obtained by using `useOtherIds()`), you can call
293
- * this selector deep down in your component stack to only have the component
294
- * re-render if properties for this particular connection change.
339
+ * Given a connection ID (as obtained by using `useOthersConnectionIds`), you
340
+ * can call this selector deep down in your component stack to only have the
341
+ * component re-render if properties for this particular user change.
295
342
  *
296
343
  * @example
297
344
  * // Returns only the selected values re-renders whenever that selection changes)
@@ -299,84 +346,429 @@ declare type RoomContextBundle<TPresence extends JsonObject, TStorage extends Ls
299
346
  */
300
347
  useOther<T>(connectionId: number, selector: (other: User<TPresence, TUserMeta>) => T, isEqual?: (prev: T, curr: T) => boolean): T;
301
348
  /**
302
- * Returns the Room of the nearest RoomProvider above in the React component
303
- * tree.
304
- */
305
- useRoom(): Room<TPresence, TStorage, TUserMeta, TRoomEvent>;
306
- /**
307
- * Gets the current user once it is connected to the room.
349
+ * useUpdateMyPresence is similar to useMyPresence but it only returns the function to update the current user presence.
350
+ * If you don't use the current user presence in your component, but you need to update it (e.g. live cursor), it's better to use useUpdateMyPresence to avoid unnecessary renders.
308
351
  *
309
352
  * @example
310
- * const me = useSelf();
311
- * const { x, y } = me.presence.cursor;
353
+ * const updateMyPresence = useUpdateMyPresence();
354
+ * updateMyPresence({ x: 0 });
355
+ * updateMyPresence({ y: 0 });
356
+ *
357
+ * // At the next render, the presence of the current user will be equal to "{ x: 0, y: 0 }"
312
358
  */
313
- useSelf(): User<TPresence, TUserMeta> | null;
359
+ useUpdateMyPresence(): (patch: Partial<TPresence>, options?: {
360
+ addToHistory: boolean;
361
+ }) => void;
314
362
  /**
315
- * Extract arbitrary data based on the current user.
363
+ * Create a callback function that lets you mutate Liveblocks state.
316
364
  *
317
- * The selector function will get re-evaluated any time your presence data
318
- * changes.
365
+ * The first argument that gets passed into your callback will be a "mutation
366
+ * context", which exposes the following:
319
367
  *
320
- * The component that uses this hook will automatically re-render if your
321
- * selector function returns a different value from its previous run.
368
+ * - `root` - The mutable Storage root.
369
+ * You can normal mutation on Live structures with this, for
370
+ * example: root.get('layers').get('layer1').set('fill', 'red')
322
371
  *
323
- * By default `useSelf()` uses strict `===` to check for equality. Take extra
324
- * care when returning a computed object or list, for example when you return
325
- * the result of a .map() or .filter() call from the selector. In those
326
- * cases, you'll probably want to use a `shallow` comparison check.
372
+ * - `setMyPresence` - Call this with a new (partial) Presence value.
327
373
  *
328
- * Will return `null` while Liveblocks isn't connected to a room yet.
374
+ * - `self` - A read-only version of the latest self, if you need it to
375
+ * compute the next state.
329
376
  *
330
- * @example
331
- * const cursor = useSelf(me => me.presence.cursor);
332
- * if (cursor !== null) {
333
- * const { x, y } = cursor;
334
- * }
377
+ * - `others` - A read-only version of the latest others list, if you need
378
+ * it to compute the next state.
335
379
  *
336
- */
337
- useSelf<T>(selector: (me: User<TPresence, TUserMeta>) => T, isEqual?: (prev: T, curr: T) => boolean): T | null;
338
- /**
339
- * Returns the mutable (!) Storage root. This hook exists for
340
- * backward-compatible reasons.
380
+ * useMutation is like React's useCallback, except that the first argument
381
+ * that gets passed into your callback will be a "mutation context".
341
382
  *
342
- * @example
343
- * const [root] = useStorageRoot();
344
- */
345
- useStorageRoot(): [root: LiveObject<TStorage> | null];
346
- /**
347
- * useUpdateMyPresence is similar to useMyPresence but it only returns the function to update the current user presence.
348
- * If you don't use the current user presence in your component, but you need to update it (e.g. live cursor), it's better to use useUpdateMyPresence to avoid unnecessary renders.
383
+ * If you want get access to the immutable root somewhere in your mutation,
384
+ * you can use `root.ToImmutable()`.
349
385
  *
350
386
  * @example
351
- * const updateMyPresence = useUpdateMyPresence();
352
- * updateMyPresence({ x: 0 });
353
- * updateMyPresence({ y: 0 });
387
+ * const fillLayers = useMutation(
388
+ * ({ root }, color: Color) => {
389
+ * ...
390
+ * },
391
+ * [],
392
+ * );
354
393
  *
355
- * // At the next render, the presence of the current user will be equal to "{ x: 0, y: 0 }"
394
+ * fillLayers('red');
395
+ *
396
+ * const deleteLayers = useMutation(
397
+ * ({ root }) => {
398
+ * ...
399
+ * },
400
+ * [],
401
+ * );
402
+ *
403
+ * deleteLayers();
356
404
  */
357
- useUpdateMyPresence(): (patch: Partial<TPresence>, options?: {
358
- addToHistory: boolean;
359
- }) => void;
360
- useMutation<F extends (context: MutationContext<TPresence, TStorage>, ...args: any[]) => any>(callback: F, deps?: unknown[]): OmitFirstArg<F>;
405
+ useMutation<F extends (context: MutationContext<TPresence, TStorage, TUserMeta>, ...args: any[]) => any>(callback: F, deps: readonly unknown[]): OmitFirstArg<F>;
361
406
  suspense: {
407
+ /**
408
+ * You normally don't need to directly interact with the RoomContext, but
409
+ * it can be necessary if you're building an advanced app where you need to
410
+ * set up a context bridge between two React renderers.
411
+ */
412
+ RoomContext: React.Context<Room<TPresence, TStorage, TUserMeta, TRoomEvent> | null>;
413
+ /**
414
+ * Makes a Room available in the component hierarchy below.
415
+ * When this component is unmounted, the current user leave the room.
416
+ * That means that you can't have 2 RoomProvider with the same room id in your react tree.
417
+ */
418
+ RoomProvider(props: RoomProviderProps<TPresence, TStorage>): JSX.Element;
419
+ /**
420
+ * Returns the Room of the nearest RoomProvider above in the React component
421
+ * tree.
422
+ */
423
+ useRoom(): Room<TPresence, TStorage, TUserMeta, TRoomEvent>;
424
+ /**
425
+ * Returns a function that batches modifications made during the given function.
426
+ * All the modifications are sent to other clients in a single message.
427
+ * All the modifications are merged in a single history item (undo/redo).
428
+ * All the subscribers are called only after the batch is over.
429
+ */
430
+ useBatch<T>(): (callback: () => T) => T;
431
+ /**
432
+ * Returns a callback that lets you broadcast custom events to other users in the room
433
+ *
434
+ * @example
435
+ * const broadcast = useBroadcastEvent();
436
+ *
437
+ * broadcast({ type: "CUSTOM_EVENT", data: { x: 0, y: 0 } });
438
+ */
439
+ useBroadcastEvent(): (event: TRoomEvent, options?: BroadcastOptions) => void;
440
+ /**
441
+ * useErrorListener is a react hook that lets you react to potential room connection errors.
442
+ *
443
+ * @example
444
+ * useErrorListener(er => {
445
+ * console.error(er);
446
+ * })
447
+ */
448
+ useErrorListener(callback: (err: Error) => void): void;
449
+ /**
450
+ * useEventListener is a react hook that lets you react to event broadcasted by other users in the room.
451
+ *
452
+ * @example
453
+ * useEventListener(({ connectionId, event }) => {
454
+ * if (event.type === "CUSTOM_EVENT") {
455
+ * // Do something
456
+ * }
457
+ * });
458
+ */
459
+ useEventListener(callback: (eventData: {
460
+ connectionId: number;
461
+ event: TRoomEvent;
462
+ }) => void): void;
463
+ /**
464
+ * Returns the room.history
465
+ */
466
+ useHistory(): History;
467
+ /**
468
+ * Returns a function that undoes the last operation executed by the current client.
469
+ * It does not impact operations made by other clients.
470
+ */
471
+ useUndo(): () => void;
472
+ /**
473
+ * Returns a function that redoes the last operation executed by the current client.
474
+ * It does not impact operations made by other clients.
475
+ */
476
+ useRedo(): () => void;
477
+ /**
478
+ * Returns whether there are any operations to undo.
479
+ */
480
+ useCanUndo(): boolean;
481
+ /**
482
+ * Returns whether there are any operations to redo.
483
+ */
484
+ useCanRedo(): boolean;
485
+ /**
486
+ * Returns the mutable (!) Storage root. This hook exists for
487
+ * backward-compatible reasons.
488
+ *
489
+ * @example
490
+ * const [root] = useStorageRoot();
491
+ */
492
+ useStorageRoot(): [root: LiveObject<TStorage> | null];
493
+ /**
494
+ * Returns your entire Liveblocks Storage as an immutable data structure.
495
+ *
496
+ * @example
497
+ * const root = useStorage();
498
+ */
362
499
  useStorage(): ToImmutable<TStorage>;
500
+ /**
501
+ * Extract arbitrary data from the Liveblocks Storage state, using an
502
+ * arbitrary selector function.
503
+ *
504
+ * The selector function will get re-evaluated any time something changes in
505
+ * Storage. The value returned by your selector function will also be the
506
+ * value returned by the hook.
507
+ *
508
+ * The `root` value that gets passed to your selector function is
509
+ * a immutable/readonly version of your Liveblocks storage root.
510
+ *
511
+ * The component that uses this hook will automatically re-render if the
512
+ * returned value changes.
513
+ *
514
+ * By default `useStorage()` uses strict `===` to check for equality. Take
515
+ * extra care when returning a computed object or list, for example when you
516
+ * return the result of a .map() or .filter() call from the selector. In
517
+ * those cases, you'll probably want to use a `shallow` comparison check.
518
+ */
363
519
  useStorage<T>(selector: (root: ToImmutable<TStorage>) => T, isEqual?: (prev: T, curr: T) => boolean): T;
520
+ /**
521
+ * Gets the current user once it is connected to the room.
522
+ *
523
+ * @example
524
+ * const me = useSelf();
525
+ * const { x, y } = me.presence.cursor;
526
+ */
364
527
  useSelf(): User<TPresence, TUserMeta>;
528
+ /**
529
+ * Extract arbitrary data based on the current user.
530
+ *
531
+ * The selector function will get re-evaluated any time your presence data
532
+ * changes.
533
+ *
534
+ * The component that uses this hook will automatically re-render if your
535
+ * selector function returns a different value from its previous run.
536
+ *
537
+ * By default `useSelf()` uses strict `===` to check for equality. Take extra
538
+ * care when returning a computed object or list, for example when you return
539
+ * the result of a .map() or .filter() call from the selector. In those
540
+ * cases, you'll probably want to use a `shallow` comparison check.
541
+ *
542
+ * Will return `null` while Liveblocks isn't connected to a room yet.
543
+ *
544
+ * @example
545
+ * const cursor = useSelf(me => me.presence.cursor);
546
+ * if (cursor !== null) {
547
+ * const { x, y } = cursor;
548
+ * }
549
+ *
550
+ */
365
551
  useSelf<T>(selector: (me: User<TPresence, TUserMeta>) => T, isEqual?: (prev: T, curr: T) => boolean): T;
552
+ /**
553
+ * Returns the presence of the current user of the current room, and a function to update it.
554
+ * It is different from the setState function returned by the useState hook from React.
555
+ * You don't need to pass the full presence object to update it.
556
+ *
557
+ * @example
558
+ * const [myPresence, updateMyPresence] = useMyPresence();
559
+ * updateMyPresence({ x: 0 });
560
+ * updateMyPresence({ y: 0 });
561
+ *
562
+ * // At the next render, "myPresence" will be equal to "{ x: 0, y: 0 }"
563
+ */
564
+ useMyPresence(): [
565
+ TPresence,
566
+ (patch: Partial<TPresence>, options?: {
567
+ addToHistory: boolean;
568
+ }) => void
569
+ ];
570
+ /**
571
+ * Returns an object that lets you get information about all the users
572
+ * currently connected in the room.
573
+ *
574
+ * @example
575
+ * const others = useOthers();
576
+ *
577
+ * // Example to map all cursors in JSX
578
+ * return (
579
+ * <>
580
+ * {others.map((user) => {
581
+ * if (user.presence.cursor == null) {
582
+ * return null;
583
+ * }
584
+ * return <Cursor key={user.connectionId} cursor={user.presence.cursor} />
585
+ * })}
586
+ * </>
587
+ * )
588
+ */
366
589
  useOthers(): Others<TPresence, TUserMeta>;
590
+ /**
591
+ * Extract arbitrary data based on all the users currently connected in the
592
+ * room (except yourself).
593
+ *
594
+ * The selector function will get re-evaluated any time a user enters or
595
+ * leaves the room, as well as whenever their presence data changes.
596
+ *
597
+ * The component that uses this hook will automatically re-render if your
598
+ * selector function returns a different value from its previous run.
599
+ *
600
+ * By default `useOthers()` uses strict `===` to check for equality. Take
601
+ * extra care when returning a computed object or list, for example when you
602
+ * return the result of a .map() or .filter() call from the selector. In
603
+ * those cases, you'll probably want to use a `shallow` comparison check.
604
+ *
605
+ * @example
606
+ * const avatars = useOthers(users => users.map(u => u.info.avatar), shallow);
607
+ * const cursors = useOthers(users => users.map(u => u.presence.cursor), shallow);
608
+ * const someoneIsTyping = useOthers(users => users.some(u => u.presence.isTyping));
609
+ *
610
+ */
367
611
  useOthers<T>(selector: (others: Others<TPresence, TUserMeta>) => T, isEqual?: (prev: T, curr: T) => boolean): T;
368
- useOtherIds(): readonly number[];
369
- useOtherIds<T>(itemSelector: (other: User<TPresence, TUserMeta>) => T, isEqual?: (prev: T, curr: T) => boolean): readonly {
370
- readonly connectionId: number;
371
- readonly data: T;
372
- }[];
612
+ /**
613
+ * Returns an array of connection IDs. This matches the values you'll get by
614
+ * using the `useOthers()` hook.
615
+ *
616
+ * Roughly equivalent to:
617
+ * useOthers((others) => others.map(other => other.connectionId), shallow)
618
+ *
619
+ * This is useful in particular to implement efficiently rendering components
620
+ * for each user in the room, e.g. cursors.
621
+ *
622
+ * @example
623
+ * const ids = useOthersConnectionIds();
624
+ * // [2, 4, 7]
625
+ */
626
+ useOthersConnectionIds(): readonly number[];
627
+ /**
628
+ * Related to useOthers(), but optimized for selecting only "subsets" of
629
+ * others. This is useful for performance reasons in particular, because
630
+ * selecting only a subset of users also means limiting the number of
631
+ * re-renders that will be triggered.
632
+ *
633
+ * @example
634
+ * const avatars = useOthersMapped(user => user.info.avatar);
635
+ * // ^^^^^^^
636
+ * // { connectionId: number; data: string }[]
637
+ *
638
+ * The selector function you pass to useOthersMapped() is called an "item
639
+ * selector", and operates on a single user at a time. If you provide an
640
+ * (optional) "item comparison" function, it will be used to compare each
641
+ * item pairwise.
642
+ *
643
+ * For example, to select multiple properties:
644
+ *
645
+ * @example
646
+ * const avatarsAndCursors = useOthersMapped(
647
+ * user => [u.info.avatar, u.presence.cursor],
648
+ * shallow, // 👈
649
+ * );
650
+ */
651
+ useOthersMapped<T>(itemSelector: (other: User<TPresence, TUserMeta>) => T, itemIsEqual?: (prev: T, curr: T) => boolean): ReadonlyArray<readonly [connectionId: number, data: T]>;
652
+ /**
653
+ * Given a connection ID (as obtained by using `useOthersConnectionIds`),
654
+ * you can call this selector deep down in your component stack to only
655
+ * have the component re-render if properties for this particular user
656
+ * change.
657
+ *
658
+ * @example
659
+ * // Returns full user and re-renders whenever anything on the user changes
660
+ * const secondUser = useOther(2);
661
+ */
373
662
  useOther(connectionId: number): User<TPresence, TUserMeta>;
663
+ /**
664
+ * Given a connection ID (as obtained by using `useOthersConnectionIds`),
665
+ * you can call this selector deep down in your component stack to only
666
+ * have the component re-render if properties for this particular user
667
+ * change.
668
+ *
669
+ * @example
670
+ * // Returns only the selected values re-renders whenever that selection changes)
671
+ * const { x, y } = useOther(2, user => user.presence.cursor);
672
+ */
374
673
  useOther<T>(connectionId: number, selector: (other: User<TPresence, TUserMeta>) => T, isEqual?: (prev: T, curr: T) => boolean): T;
674
+ /**
675
+ * useUpdateMyPresence is similar to useMyPresence but it only returns the function to update the current user presence.
676
+ * If you don't use the current user presence in your component, but you need to update it (e.g. live cursor), it's better to use useUpdateMyPresence to avoid unnecessary renders.
677
+ *
678
+ * @example
679
+ * const updateMyPresence = useUpdateMyPresence();
680
+ * updateMyPresence({ x: 0 });
681
+ * updateMyPresence({ y: 0 });
682
+ *
683
+ * // At the next render, the presence of the current user will be equal to "{ x: 0, y: 0 }"
684
+ */
685
+ useUpdateMyPresence(): (patch: Partial<TPresence>, options?: {
686
+ addToHistory: boolean;
687
+ }) => void;
688
+ /**
689
+ * Create a callback function that lets you mutate Liveblocks state.
690
+ *
691
+ * The first argument that gets passed into your callback will be
692
+ * a "mutation context", which exposes the following:
693
+ *
694
+ * - `root` - The mutable Storage root.
695
+ * You can normal mutation on Live structures with this, for
696
+ * example: root.get('layers').get('layer1').set('fill',
697
+ * 'red')
698
+ *
699
+ * - `setMyPresence` - Call this with a new (partial) Presence value.
700
+ *
701
+ * - `self` - A read-only version of the latest self, if you need it to
702
+ * compute the next state.
703
+ *
704
+ * - `others` - A read-only version of the latest others list, if you
705
+ * need it to compute the next state.
706
+ *
707
+ * useMutation is like React's useCallback, except that the first argument
708
+ * that gets passed into your callback will be a "mutation context".
709
+ *
710
+ * If you want get access to the immutable root somewhere in your mutation,
711
+ * you can use `root.ToImmutable()`.
712
+ *
713
+ * @example
714
+ * const fillLayers = useMutation(
715
+ * ({ root }, color: Color) => {
716
+ * ...
717
+ * },
718
+ * [],
719
+ * );
720
+ *
721
+ * fillLayers('red');
722
+ *
723
+ * const deleteLayers = useMutation(
724
+ * ({ root }) => {
725
+ * ...
726
+ * },
727
+ * [],
728
+ * );
729
+ *
730
+ * deleteLayers();
731
+ */
732
+ useMutation<F extends (context: MutationContext<TPresence, TStorage, TUserMeta>, ...args: any[]) => any>(callback: F, deps: readonly unknown[]): OmitFirstArg<F>;
733
+ /**
734
+ * Returns the LiveList associated with the provided key. The hook triggers
735
+ * a re-render if the LiveList is updated, however it does not triggers
736
+ * a re-render if a nested CRDT is updated.
737
+ *
738
+ * @param key The storage key associated with the LiveList
739
+ * @returns null while the storage is loading, otherwise, returns the LiveList associated to the storage
740
+ *
741
+ * @example
742
+ * const animals = useList("animals"); // e.g. [] or ["🦁", "🐍", "🦍"]
743
+ */
375
744
  useList<TKey extends Extract<keyof TStorage, string>>(key: TKey): TStorage[TKey];
745
+ /**
746
+ * Returns the LiveMap associated with the provided key. If the LiveMap
747
+ * does not exist, a new empty LiveMap will be created. The hook triggers
748
+ * a re-render if the LiveMap is updated, however it does not triggers
749
+ * a re-render if a nested CRDT is updated.
750
+ *
751
+ * @param key The storage key associated with the LiveMap
752
+ * @returns null while the storage is loading, otherwise, returns the LiveMap associated to the storage
753
+ *
754
+ * @example
755
+ * const shapesById = useMap("shapes");
756
+ */
376
757
  useMap<TKey extends Extract<keyof TStorage, string>>(key: TKey): TStorage[TKey];
758
+ /**
759
+ * Returns the LiveObject associated with the provided key.
760
+ * The hook triggers a re-render if the LiveObject is updated, however it does not triggers a re-render if a nested CRDT is updated.
761
+ *
762
+ * @param key The storage key associated with the LiveObject
763
+ * @returns null while the storage is loading, otherwise, returns the LveObject associated to the storage
764
+ *
765
+ * @example
766
+ * const object = useObject("obj");
767
+ */
377
768
  useObject<TKey extends Extract<keyof TStorage, string>>(key: TKey): TStorage[TKey];
378
769
  };
379
770
  };
771
+
380
772
  declare function createRoomContext<TPresence extends JsonObject, TStorage extends LsonObject = LsonObject, TUserMeta extends BaseUserMeta = BaseUserMeta, TRoomEvent extends Json = never>(client: Client): RoomContextBundle<TPresence, TStorage, TUserMeta, TRoomEvent>;
381
773
 
382
774
  export { ClientSideSuspense, MutationContext, createRoomContext };
package/index.js CHANGED
@@ -33,16 +33,46 @@ function useInitial(value) {
33
33
  var noop = () => {
34
34
  };
35
35
  var identity = (x) => x;
36
+ function useSyncExternalStore(s, g, gg) {
37
+ return _withselector.useSyncExternalStoreWithSelector.call(void 0, s, g, gg, identity);
38
+ }
36
39
  var EMPTY_OTHERS = _internal.asArrayWithLegacyMethods.call(void 0, []);
37
40
  function getEmptyOthers() {
38
41
  return EMPTY_OTHERS;
39
42
  }
43
+ function makeMutationContext(room) {
44
+ const errmsg = "This mutation cannot be used until connected to the Liveblocks room";
45
+ return {
46
+ get storage() {
47
+ const mutableRoot = room.getStorageSnapshot();
48
+ if (mutableRoot === null) {
49
+ throw new Error(errmsg);
50
+ }
51
+ return mutableRoot;
52
+ },
53
+ get self() {
54
+ const self = room.getSelf();
55
+ if (self === null) {
56
+ throw new Error(errmsg);
57
+ }
58
+ return self;
59
+ },
60
+ get others() {
61
+ const others = room.getOthers();
62
+ if (!room.isSelfAware()) {
63
+ throw new Error(errmsg);
64
+ }
65
+ return others;
66
+ },
67
+ setMyPresence: room.updatePresence
68
+ };
69
+ }
40
70
  function createRoomContext(client) {
41
71
  const RoomContext = React2.createContext(null);
42
72
  function RoomProvider(props) {
43
73
  const { id: roomId, initialPresence, initialStorage } = props;
44
74
  if (process.env.NODE_ENV !== "production") {
45
- if (roomId == null) {
75
+ if (!roomId) {
46
76
  throw new Error(
47
77
  "RoomProvider id property is required. For more information: https://liveblocks.io/docs/errors/liveblocks-react/RoomProvider-id-property-is-required"
48
78
  );
@@ -83,17 +113,17 @@ function createRoomContext(client) {
83
113
  }
84
114
  function useRoom() {
85
115
  const room = React2.useContext(RoomContext);
86
- if (room == null) {
116
+ if (room === null) {
87
117
  throw new Error("RoomProvider is missing from the react tree");
88
118
  }
89
119
  return room;
90
120
  }
91
121
  function useMyPresence() {
92
122
  const room = useRoom();
93
- const presence = room.getPresence();
94
- const rerender = useRerender();
123
+ const subscribe = room.events.me.subscribe;
124
+ const getSnapshot = room.getPresence;
125
+ const presence = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
95
126
  const setPresence = room.updatePresence;
96
- React2.useEffect(() => room.events.me.subscribe(rerender), [room, rerender]);
97
127
  return [presence, setPresence];
98
128
  }
99
129
  function useUpdateMyPresence() {
@@ -112,31 +142,27 @@ function createRoomContext(client) {
112
142
  isEqual
113
143
  );
114
144
  }
115
- function useOtherIds(itemSelector, isEqual) {
116
- const _useCallback = React2.useCallback;
117
- const _useOthers = useOthers;
118
- if (itemSelector === void 0) {
119
- return _useOthers(connectionIdSelector, _client.shallow);
120
- } else {
121
- const wrappedSelector = _useCallback(
122
- (others) => others.map((other) => ({
123
- connectionId: other.connectionId,
124
- data: itemSelector(other)
125
- })),
126
- [itemSelector]
127
- );
128
- const wrappedIsEqual = _useCallback(
129
- (a, b) => {
130
- const eq = isEqual != null ? isEqual : Object.is;
131
- return a.length === b.length && a.every((atuple, index) => {
132
- const btuple = b[index];
133
- return atuple.connectionId === btuple.connectionId && eq(atuple.data, btuple.data);
134
- });
135
- },
136
- [isEqual]
137
- );
138
- return _useOthers(wrappedSelector, wrappedIsEqual);
139
- }
145
+ function useOthersConnectionIds() {
146
+ return useOthers(connectionIdSelector, _client.shallow);
147
+ }
148
+ function useOthersMapped(itemSelector, itemIsEqual) {
149
+ const wrappedSelector = React2.useCallback(
150
+ (others) => others.map(
151
+ (other) => [other.connectionId, itemSelector(other)]
152
+ ),
153
+ [itemSelector]
154
+ );
155
+ const wrappedIsEqual = React2.useCallback(
156
+ (a, b) => {
157
+ const eq = itemIsEqual != null ? itemIsEqual : Object.is;
158
+ return a.length === b.length && a.every((atuple, index) => {
159
+ const btuple = b[index];
160
+ return atuple[0] === btuple[0] && eq(atuple[1], btuple[1]);
161
+ });
162
+ },
163
+ [itemIsEqual]
164
+ );
165
+ return useOthers(wrappedSelector, wrappedIsEqual);
140
166
  }
141
167
  const sentinel = Symbol();
142
168
  function useOther(connectionId, selector, isEqual) {
@@ -249,13 +275,7 @@ function createRoomContext(client) {
249
275
  const subscribe = room.events.storageDidLoad.subscribeOnce;
250
276
  const getSnapshot = room.getStorageSnapshot;
251
277
  const getServerSnapshot = React2.useCallback(() => null, []);
252
- const selector = identity;
253
- return _withselector.useSyncExternalStoreWithSelector.call(void 0,
254
- subscribe,
255
- getSnapshot,
256
- getServerSnapshot,
257
- selector
258
- );
278
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
259
279
  }
260
280
  function useStorageRoot() {
261
281
  return [useMutableStorageRoot()];
@@ -271,21 +291,15 @@ function createRoomContext(client) {
271
291
  }
272
292
  function useCanUndo() {
273
293
  const room = useRoom();
274
- const [canUndo, setCanUndo] = React2.useState(room.history.canUndo);
275
- React2.useEffect(
276
- () => room.events.history.subscribe(({ canUndo: canUndo2 }) => setCanUndo(canUndo2)),
277
- [room]
278
- );
279
- return canUndo;
294
+ const subscribe = room.events.history.subscribe;
295
+ const canUndo = room.history.canUndo;
296
+ return useSyncExternalStore(subscribe, canUndo, canUndo);
280
297
  }
281
298
  function useCanRedo() {
282
299
  const room = useRoom();
283
- const [canRedo, setCanRedo] = React2.useState(room.history.canRedo);
284
- React2.useEffect(
285
- () => room.events.history.subscribe(({ canRedo: canRedo2 }) => setCanRedo(canRedo2)),
286
- [room]
287
- );
288
- return canRedo;
300
+ const subscribe = room.events.history.subscribe;
301
+ const canRedo = room.history.canRedo;
302
+ return useSyncExternalStore(subscribe, canRedo, canRedo);
289
303
  }
290
304
  function useBatch() {
291
305
  return useRoom().batch;
@@ -295,7 +309,7 @@ function createRoomContext(client) {
295
309
  const root = useMutableStorageRoot();
296
310
  const rerender = useRerender();
297
311
  React2.useEffect(() => {
298
- if (root == null) {
312
+ if (root === null) {
299
313
  return;
300
314
  }
301
315
  let liveValue = root.get(key);
@@ -325,7 +339,7 @@ function createRoomContext(client) {
325
339
  unsubscribeCrdt();
326
340
  };
327
341
  }, [root, room, key, rerender]);
328
- if (root == null) {
342
+ if (root === null) {
329
343
  return null;
330
344
  } else {
331
345
  return root.get(key);
@@ -364,7 +378,7 @@ function createRoomContext(client) {
364
378
  function ensureNotServerSide() {
365
379
  if (typeof window === "undefined") {
366
380
  throw new Error(
367
- "You cannot call the Suspense version of this hook on the server side. Make sure to only call them on the client side.\nFor tips for structuring your app, see XXX"
381
+ "You cannot use the Suspense version of this hook on the server side. Make sure to only call them on the client side.\nFor tips, see https://liveblocks.io/docs/api-reference/liveblocks-react#suspense-avoid-ssr"
368
382
  );
369
383
  }
370
384
  }
@@ -390,27 +404,13 @@ function createRoomContext(client) {
390
404
  }
391
405
  function useMutation(callback, deps) {
392
406
  const room = useRoom();
393
- const root = useMutableStorageRoot();
394
- const setMyPresence = room.updatePresence;
395
407
  return React2.useMemo(
396
408
  () => {
397
- if (root !== null) {
398
- const mutationCtx = {
399
- root,
400
- setMyPresence
401
- };
402
- return (...args) => room.batch(
403
- () => callback(mutationCtx, ...args)
404
- );
405
- } else {
406
- return () => {
407
- throw new Error(
408
- "Mutation cannot be called while Liveblocks Storage has not loaded yet"
409
- );
410
- };
411
- }
409
+ return (...args) => room.batch(
410
+ () => callback(makeMutationContext(room), ...args)
411
+ );
412
412
  },
413
- deps !== void 0 ? [root, room, setMyPresence, ...deps] : [root, room, setMyPresence, callback]
413
+ [room, ...deps]
414
414
  );
415
415
  }
416
416
  function useStorageSuspense(selector, isEqual) {
@@ -434,12 +434,13 @@ function createRoomContext(client) {
434
434
  isEqual
435
435
  );
436
436
  }
437
- function useOtherIdsSuspense(itemSelector, isEqual) {
437
+ function useOthersConnectionIdsSuspense() {
438
438
  useSuspendUntilPresenceLoaded();
439
- return useOtherIds(
440
- itemSelector,
441
- isEqual
442
- );
439
+ return useOthersConnectionIds();
440
+ }
441
+ function useOthersMappedSuspense(itemSelector, itemIsEqual) {
442
+ useSuspendUntilPresenceLoaded();
443
+ return useOthersMapped(itemSelector, itemIsEqual);
443
444
  }
444
445
  function useOtherSuspense(connectionId, selector, isEqual) {
445
446
  useSuspendUntilPresenceLoaded();
@@ -454,39 +455,57 @@ function createRoomContext(client) {
454
455
  return useLegacyKey(key);
455
456
  }
456
457
  return {
458
+ RoomContext,
457
459
  RoomProvider,
460
+ useRoom,
458
461
  useBatch,
459
462
  useBroadcastEvent,
460
- useCanRedo,
461
- useCanUndo,
462
463
  useErrorListener,
463
464
  useEventListener,
464
465
  useHistory,
465
- useMyPresence,
466
- useOthers,
467
- useOtherIds,
468
- useOther,
466
+ useUndo,
469
467
  useRedo,
470
- useRoom,
471
- useSelf,
468
+ useCanRedo,
469
+ useCanUndo,
470
+ useList: useLegacyKey,
471
+ useMap: useLegacyKey,
472
+ useObject: useLegacyKey,
472
473
  useStorageRoot,
473
474
  useStorage,
474
- useUndo,
475
+ useSelf,
476
+ useMyPresence,
475
477
  useUpdateMyPresence,
478
+ useOthers,
479
+ useOthersMapped,
480
+ useOthersConnectionIds,
481
+ useOther,
476
482
  useMutation,
477
- useList: useLegacyKey,
478
- useMap: useLegacyKey,
479
- useObject: useLegacyKey,
480
- RoomContext,
481
483
  suspense: {
484
+ RoomContext,
485
+ RoomProvider,
486
+ useRoom,
487
+ useBatch,
488
+ useBroadcastEvent,
489
+ useErrorListener,
490
+ useEventListener,
491
+ useHistory,
492
+ useUndo,
493
+ useRedo,
494
+ useCanRedo,
495
+ useCanUndo,
496
+ useList: useLegacyKeySuspense,
497
+ useMap: useLegacyKeySuspense,
498
+ useObject: useLegacyKeySuspense,
499
+ useStorageRoot,
482
500
  useStorage: useStorageSuspense,
483
501
  useSelf: useSelfSuspense,
502
+ useMyPresence,
503
+ useUpdateMyPresence,
484
504
  useOthers: useOthersSuspense,
485
- useOtherIds: useOtherIdsSuspense,
505
+ useOthersMapped: useOthersMappedSuspense,
506
+ useOthersConnectionIds: useOthersConnectionIdsSuspense,
486
507
  useOther: useOtherSuspense,
487
- useList: useLegacyKeySuspense,
488
- useMap: useLegacyKeySuspense,
489
- useObject: useLegacyKeySuspense
508
+ useMutation
490
509
  }
491
510
  };
492
511
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liveblocks/react",
3
- "version": "0.18.0-beta2",
3
+ "version": "0.18.0-beta4",
4
4
  "description": "A set of React hooks and providers to use Liveblocks declaratively.",
5
5
  "main": "./index.js",
6
6
  "module": "./index.mjs",
@@ -27,7 +27,7 @@
27
27
  "use-sync-external-store": "^1.2.0"
28
28
  },
29
29
  "peerDependencies": {
30
- "@liveblocks/client": "0.18.0-beta2",
30
+ "@liveblocks/client": "0.18.0-beta4",
31
31
  "react": "^16.14.0 || ^17 || ^18"
32
32
  },
33
33
  "repository": {