@liveblocks/react 0.18.0-beta2 → 0.18.0-beta3

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
+ ee7b9e79d3549611f64024b4247e9a862cfddd34
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> = {
41
+ declare type MutationContext<TPresence extends JsonObject, TStorage extends LsonObject, TUserMeta extends BaseUserMeta> = {
36
42
  root: 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,52 @@ 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 = useConnectionIds();
300
+ * // [2, 4, 7]
249
301
  */
250
- useOtherIds(): readonly number[];
302
+ useConnectionIds(): 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 = useOthersWithData(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 useOthersWithData() 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 = useOthersWithData(
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 {
327
+ useOthersWithData<T>(itemSelector: (other: User<TPresence, TUserMeta>) => T, itemIsEqual?: (prev: T, curr: T) => boolean): readonly {
278
328
  readonly connectionId: number;
279
329
  readonly data: T;
280
330
  }[];
281
331
  /**
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.
332
+ * Given a connection ID (as obtained by using `useConnectionIds()`), you can
333
+ * call this selector deep down in your component stack to only have the
334
+ * component re-render if properties for this particular user change.
285
335
  *
286
336
  * @example
287
337
  * // Returns full user and re-renders whenever anything on the user changes
@@ -289,9 +339,9 @@ declare type RoomContextBundle<TPresence extends JsonObject, TStorage extends Ls
289
339
  */
290
340
  useOther(connectionId: number): User<TPresence, TUserMeta>;
291
341
  /**
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.
342
+ * Given a connection ID (as obtained by using `useConnectionIds()`), you can
343
+ * call this selector deep down in your component stack to only have the
344
+ * component re-render if properties for this particular user change.
295
345
  *
296
346
  * @example
297
347
  * // Returns only the selected values re-renders whenever that selection changes)
@@ -299,84 +349,431 @@ declare type RoomContextBundle<TPresence extends JsonObject, TStorage extends Ls
299
349
  */
300
350
  useOther<T>(connectionId: number, selector: (other: User<TPresence, TUserMeta>) => T, isEqual?: (prev: T, curr: T) => boolean): T;
301
351
  /**
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.
352
+ * useUpdateMyPresence is similar to useMyPresence but it only returns the function to update the current user presence.
353
+ * 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
354
  *
309
355
  * @example
310
- * const me = useSelf();
311
- * const { x, y } = me.presence.cursor;
356
+ * const updateMyPresence = useUpdateMyPresence();
357
+ * updateMyPresence({ x: 0 });
358
+ * updateMyPresence({ y: 0 });
359
+ *
360
+ * // At the next render, the presence of the current user will be equal to "{ x: 0, y: 0 }"
312
361
  */
313
- useSelf(): User<TPresence, TUserMeta> | null;
362
+ useUpdateMyPresence(): (patch: Partial<TPresence>, options?: {
363
+ addToHistory: boolean;
364
+ }) => void;
314
365
  /**
315
- * Extract arbitrary data based on the current user.
366
+ * Create a callback function that can be called to mutate Liveblocks state.
316
367
  *
317
- * The selector function will get re-evaluated any time your presence data
318
- * changes.
368
+ * The first argument that gets passed into your callback will be a "mutation
369
+ * context", which exposes the following:
319
370
  *
320
- * The component that uses this hook will automatically re-render if your
321
- * selector function returns a different value from its previous run.
371
+ * - `root` - The mutable Storage root.
372
+ * You can normal mutation on Live structures with this, for
373
+ * example: root.get('layers').get('layer1').set('fill', 'red')
322
374
  *
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.
375
+ * - `setMyPresence` - Call this with a new (partial) Presence value.
327
376
  *
328
- * Will return `null` while Liveblocks isn't connected to a room yet.
377
+ * - `self` - A read-only version of the latest self, if you need it to
378
+ * compute the next state.
329
379
  *
330
- * @example
331
- * const cursor = useSelf(me => me.presence.cursor);
332
- * if (cursor !== null) {
333
- * const { x, y } = cursor;
334
- * }
380
+ * - `others` - A read-only version of the latest others list, if you need
381
+ * it to compute the next state.
335
382
  *
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.
383
+ * useMutation is like React's useCallback, except that the first argument
384
+ * that gets passed into your callback will be a "mutation context".
341
385
  *
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.
386
+ * If you want get access to the immutable root somewhere in your mutation,
387
+ * you can use `root.ToImmutable()`.
349
388
  *
350
389
  * @example
351
- * const updateMyPresence = useUpdateMyPresence();
352
- * updateMyPresence({ x: 0 });
353
- * updateMyPresence({ y: 0 });
390
+ * const fillLayers = useMutation(
391
+ * ({ root }, color: Color) => {
392
+ * ...
393
+ * },
394
+ * [],
395
+ * );
354
396
  *
355
- * // At the next render, the presence of the current user will be equal to "{ x: 0, y: 0 }"
397
+ * fillLayers('red');
398
+ *
399
+ * const deleteLayers = useMutation(
400
+ * ({ root }) => {
401
+ * ...
402
+ * },
403
+ * [],
404
+ * );
405
+ *
406
+ * deleteLayers();
356
407
  */
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>;
408
+ useMutation<F extends (context: MutationContext<TPresence, TStorage, TUserMeta>, ...args: any[]) => any>(callback: F, deps: readonly unknown[]): OmitFirstArg<F>;
361
409
  suspense: {
410
+ /**
411
+ * You normally don't need to directly interact with the RoomContext, but
412
+ * it can be necessary if you're building an advanced app where you need to
413
+ * set up a context bridge between two React renderers.
414
+ */
415
+ RoomContext: React.Context<Room<TPresence, TStorage, TUserMeta, TRoomEvent> | null>;
416
+ /**
417
+ * Makes a Room available in the component hierarchy below.
418
+ * When this component is unmounted, the current user leave the room.
419
+ * That means that you can't have 2 RoomProvider with the same room id in your react tree.
420
+ */
421
+ RoomProvider(props: RoomProviderProps<TPresence, TStorage>): JSX.Element;
422
+ /**
423
+ * Returns the Room of the nearest RoomProvider above in the React component
424
+ * tree.
425
+ */
426
+ useRoom(): Room<TPresence, TStorage, TUserMeta, TRoomEvent>;
427
+ /**
428
+ * Returns a function that batches modifications made during the given function.
429
+ * All the modifications are sent to other clients in a single message.
430
+ * All the modifications are merged in a single history item (undo/redo).
431
+ * All the subscribers are called only after the batch is over.
432
+ */
433
+ useBatch<T>(): (callback: () => T) => T;
434
+ /**
435
+ * Returns a callback that lets you broadcast custom events to other users in the room
436
+ *
437
+ * @example
438
+ * const broadcast = useBroadcastEvent();
439
+ *
440
+ * broadcast({ type: "CUSTOM_EVENT", data: { x: 0, y: 0 } });
441
+ */
442
+ useBroadcastEvent(): (event: TRoomEvent, options?: BroadcastOptions) => void;
443
+ /**
444
+ * useErrorListener is a react hook that lets you react to potential room connection errors.
445
+ *
446
+ * @example
447
+ * useErrorListener(er => {
448
+ * console.error(er);
449
+ * })
450
+ */
451
+ useErrorListener(callback: (err: Error) => void): void;
452
+ /**
453
+ * useEventListener is a react hook that lets you react to event broadcasted by other users in the room.
454
+ *
455
+ * @example
456
+ * useEventListener(({ connectionId, event }) => {
457
+ * if (event.type === "CUSTOM_EVENT") {
458
+ * // Do something
459
+ * }
460
+ * });
461
+ */
462
+ useEventListener(callback: (eventData: {
463
+ connectionId: number;
464
+ event: TRoomEvent;
465
+ }) => void): void;
466
+ /**
467
+ * Returns the room.history
468
+ */
469
+ useHistory(): History;
470
+ /**
471
+ * Returns a function that undoes the last operation executed by the current client.
472
+ * It does not impact operations made by other clients.
473
+ */
474
+ useUndo(): () => void;
475
+ /**
476
+ * Returns a function that redoes the last operation executed by the current client.
477
+ * It does not impact operations made by other clients.
478
+ */
479
+ useRedo(): () => void;
480
+ /**
481
+ * Returns whether there are any operations to undo.
482
+ */
483
+ useCanUndo(): boolean;
484
+ /**
485
+ * Returns whether there are any operations to redo.
486
+ */
487
+ useCanRedo(): boolean;
488
+ /**
489
+ * Returns the mutable (!) Storage root. This hook exists for
490
+ * backward-compatible reasons.
491
+ *
492
+ * @example
493
+ * const [root] = useStorageRoot();
494
+ */
495
+ useStorageRoot(): [root: LiveObject<TStorage> | null];
496
+ /**
497
+ * Returns your entire Liveblocks Storage as an immutable data structure.
498
+ *
499
+ * @example
500
+ * const root = useStorage();
501
+ */
362
502
  useStorage(): ToImmutable<TStorage>;
503
+ /**
504
+ * Extract arbitrary data from the Liveblocks Storage state, using an
505
+ * arbitrary selector function.
506
+ *
507
+ * The selector function will get re-evaluated any time something changes in
508
+ * Storage. The value returned by your selector function will also be the
509
+ * value returned by the hook.
510
+ *
511
+ * The `root` value that gets passed to your selector function is
512
+ * a immutable/readonly version of your Liveblocks storage root.
513
+ *
514
+ * The component that uses this hook will automatically re-render if the
515
+ * returned value changes.
516
+ *
517
+ * By default `useStorage()` uses strict `===` to check for equality. Take
518
+ * extra care when returning a computed object or list, for example when you
519
+ * return the result of a .map() or .filter() call from the selector. In
520
+ * those cases, you'll probably want to use a `shallow` comparison check.
521
+ */
363
522
  useStorage<T>(selector: (root: ToImmutable<TStorage>) => T, isEqual?: (prev: T, curr: T) => boolean): T;
523
+ /**
524
+ * Gets the current user once it is connected to the room.
525
+ *
526
+ * @example
527
+ * const me = useSelf();
528
+ * const { x, y } = me.presence.cursor;
529
+ */
364
530
  useSelf(): User<TPresence, TUserMeta>;
531
+ /**
532
+ * Extract arbitrary data based on the current user.
533
+ *
534
+ * The selector function will get re-evaluated any time your presence data
535
+ * changes.
536
+ *
537
+ * The component that uses this hook will automatically re-render if your
538
+ * selector function returns a different value from its previous run.
539
+ *
540
+ * By default `useSelf()` uses strict `===` to check for equality. Take extra
541
+ * care when returning a computed object or list, for example when you return
542
+ * the result of a .map() or .filter() call from the selector. In those
543
+ * cases, you'll probably want to use a `shallow` comparison check.
544
+ *
545
+ * Will return `null` while Liveblocks isn't connected to a room yet.
546
+ *
547
+ * @example
548
+ * const cursor = useSelf(me => me.presence.cursor);
549
+ * if (cursor !== null) {
550
+ * const { x, y } = cursor;
551
+ * }
552
+ *
553
+ */
365
554
  useSelf<T>(selector: (me: User<TPresence, TUserMeta>) => T, isEqual?: (prev: T, curr: T) => boolean): T;
555
+ /**
556
+ * Returns the presence of the current user of the current room, and a function to update it.
557
+ * It is different from the setState function returned by the useState hook from React.
558
+ * You don't need to pass the full presence object to update it.
559
+ *
560
+ * @example
561
+ * const [myPresence, updateMyPresence] = useMyPresence();
562
+ * updateMyPresence({ x: 0 });
563
+ * updateMyPresence({ y: 0 });
564
+ *
565
+ * // At the next render, "myPresence" will be equal to "{ x: 0, y: 0 }"
566
+ */
567
+ useMyPresence(): [
568
+ TPresence,
569
+ (patch: Partial<TPresence>, options?: {
570
+ addToHistory: boolean;
571
+ }) => void
572
+ ];
573
+ /**
574
+ * Returns an object that lets you get information about all the users
575
+ * currently connected in the room.
576
+ *
577
+ * @example
578
+ * const others = useOthers();
579
+ *
580
+ * // Example to map all cursors in JSX
581
+ * return (
582
+ * <>
583
+ * {others.map((user) => {
584
+ * if (user.presence.cursor == null) {
585
+ * return null;
586
+ * }
587
+ * return <Cursor key={user.connectionId} cursor={user.presence.cursor} />
588
+ * })}
589
+ * </>
590
+ * )
591
+ */
366
592
  useOthers(): Others<TPresence, TUserMeta>;
593
+ /**
594
+ * Extract arbitrary data based on all the users currently connected in the
595
+ * room (except yourself).
596
+ *
597
+ * The selector function will get re-evaluated any time a user enters or
598
+ * leaves the room, as well as whenever their presence data changes.
599
+ *
600
+ * The component that uses this hook will automatically re-render if your
601
+ * selector function returns a different value from its previous run.
602
+ *
603
+ * By default `useOthers()` uses strict `===` to check for equality. Take
604
+ * extra care when returning a computed object or list, for example when you
605
+ * return the result of a .map() or .filter() call from the selector. In
606
+ * those cases, you'll probably want to use a `shallow` comparison check.
607
+ *
608
+ * @example
609
+ * const avatars = useOthers(users => users.map(u => u.info.avatar), shallow);
610
+ * const cursors = useOthers(users => users.map(u => u.presence.cursor), shallow);
611
+ * const someoneIsTyping = useOthers(users => users.some(u => u.presence.isTyping));
612
+ *
613
+ */
367
614
  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 {
615
+ /**
616
+ * Returns an array of connection IDs. This matches the values you'll get by
617
+ * using the `useOthers()` hook.
618
+ *
619
+ * Roughly equivalent to:
620
+ * useOthers((others) => others.map(other => other.connectionId), shallow)
621
+ *
622
+ * This is useful in particular to implement efficiently rendering components
623
+ * for each user in the room, e.g. cursors.
624
+ *
625
+ * @example
626
+ * const ids = useConnectionIds();
627
+ * // [2, 4, 7]
628
+ */
629
+ useConnectionIds(): readonly number[];
630
+ /**
631
+ * Related to useOthers(), but optimized for selecting only "subsets" of
632
+ * others. This is useful for performance reasons in particular, because
633
+ * selecting only a subset of users also means limiting the number of
634
+ * re-renders that will be triggered.
635
+ *
636
+ * @example
637
+ * const avatars = useOthersWithData(user => user.info.avatar);
638
+ * // ^^^^^^^
639
+ * // { connectionId: number; data: string }[]
640
+ *
641
+ * The selector function you pass to useOthersWithData() is called an "item
642
+ * selector", and operates on a single user at a time. If you provide an
643
+ * (optional) "item comparison" function, it will be used to compare each
644
+ * item pairwise.
645
+ *
646
+ * For example, to select multiple properties:
647
+ *
648
+ * @example
649
+ * const avatarsAndCursors = useOthersWithData(
650
+ * user => [u.info.avatar, u.presence.cursor],
651
+ * shallow, // 👈
652
+ * );
653
+ */
654
+ useOthersWithData<T>(itemSelector: (other: User<TPresence, TUserMeta>) => T, itemIsEqual?: (prev: T, curr: T) => boolean): readonly {
370
655
  readonly connectionId: number;
371
656
  readonly data: T;
372
657
  }[];
658
+ /**
659
+ * Given a connection ID (as obtained by using `useConnectionIds()`), you
660
+ * can call this selector deep down in your component stack to only have
661
+ * the component re-render if properties for this particular user change.
662
+ *
663
+ * @example
664
+ * // Returns full user and re-renders whenever anything on the user changes
665
+ * const secondUser = useOther(2);
666
+ */
373
667
  useOther(connectionId: number): User<TPresence, TUserMeta>;
668
+ /**
669
+ * Given a connection ID (as obtained by using `useConnectionIds()`), you
670
+ * can call this selector deep down in your component stack to only have
671
+ * the component re-render if properties for this particular user change.
672
+ *
673
+ * @example
674
+ * // Returns only the selected values re-renders whenever that selection changes)
675
+ * const { x, y } = useOther(2, user => user.presence.cursor);
676
+ */
374
677
  useOther<T>(connectionId: number, selector: (other: User<TPresence, TUserMeta>) => T, isEqual?: (prev: T, curr: T) => boolean): T;
678
+ /**
679
+ * useUpdateMyPresence is similar to useMyPresence but it only returns the function to update the current user presence.
680
+ * 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.
681
+ *
682
+ * @example
683
+ * const updateMyPresence = useUpdateMyPresence();
684
+ * updateMyPresence({ x: 0 });
685
+ * updateMyPresence({ y: 0 });
686
+ *
687
+ * // At the next render, the presence of the current user will be equal to "{ x: 0, y: 0 }"
688
+ */
689
+ useUpdateMyPresence(): (patch: Partial<TPresence>, options?: {
690
+ addToHistory: boolean;
691
+ }) => void;
692
+ /**
693
+ * Create a callback function that can be called to mutate Liveblocks
694
+ * state.
695
+ *
696
+ * The first argument that gets passed into your callback will be
697
+ * a "mutation context", which exposes the following:
698
+ *
699
+ * - `root` - The mutable Storage root.
700
+ * You can normal mutation on Live structures with this, for
701
+ * example: root.get('layers').get('layer1').set('fill',
702
+ * 'red')
703
+ *
704
+ * - `setMyPresence` - Call this with a new (partial) Presence value.
705
+ *
706
+ * - `self` - A read-only version of the latest self, if you need it to
707
+ * compute the next state.
708
+ *
709
+ * - `others` - A read-only version of the latest others list, if you
710
+ * need it to compute the next state.
711
+ *
712
+ * useMutation is like React's useCallback, except that the first argument
713
+ * that gets passed into your callback will be a "mutation context".
714
+ *
715
+ * If you want get access to the immutable root somewhere in your mutation,
716
+ * you can use `root.ToImmutable()`.
717
+ *
718
+ * @example
719
+ * const fillLayers = useMutation(
720
+ * ({ root }, color: Color) => {
721
+ * ...
722
+ * },
723
+ * [],
724
+ * );
725
+ *
726
+ * fillLayers('red');
727
+ *
728
+ * const deleteLayers = useMutation(
729
+ * ({ root }) => {
730
+ * ...
731
+ * },
732
+ * [],
733
+ * );
734
+ *
735
+ * deleteLayers();
736
+ */
737
+ useMutation<F extends (context: MutationContext<TPresence, TStorage, TUserMeta>, ...args: any[]) => any>(callback: F, deps: readonly unknown[]): OmitFirstArg<F>;
738
+ /**
739
+ * Returns the LiveList associated with the provided key. The hook triggers
740
+ * a re-render if the LiveList is updated, however it does not triggers
741
+ * a re-render if a nested CRDT is updated.
742
+ *
743
+ * @param key The storage key associated with the LiveList
744
+ * @returns null while the storage is loading, otherwise, returns the LiveList associated to the storage
745
+ *
746
+ * @example
747
+ * const animals = useList("animals"); // e.g. [] or ["🦁", "🐍", "🦍"]
748
+ */
375
749
  useList<TKey extends Extract<keyof TStorage, string>>(key: TKey): TStorage[TKey];
750
+ /**
751
+ * Returns the LiveMap associated with the provided key. If the LiveMap
752
+ * does not exist, a new empty LiveMap will be created. The hook triggers
753
+ * a re-render if the LiveMap is updated, however it does not triggers
754
+ * a re-render if a nested CRDT is updated.
755
+ *
756
+ * @param key The storage key associated with the LiveMap
757
+ * @returns null while the storage is loading, otherwise, returns the LiveMap associated to the storage
758
+ *
759
+ * @example
760
+ * const shapesById = useMap("shapes");
761
+ */
376
762
  useMap<TKey extends Extract<keyof TStorage, string>>(key: TKey): TStorage[TKey];
763
+ /**
764
+ * Returns the LiveObject associated with the provided key.
765
+ * 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.
766
+ *
767
+ * @param key The storage key associated with the LiveObject
768
+ * @returns null while the storage is loading, otherwise, returns the LveObject associated to the storage
769
+ *
770
+ * @example
771
+ * const object = useObject("obj");
772
+ */
377
773
  useObject<TKey extends Extract<keyof TStorage, string>>(key: TKey): TStorage[TKey];
378
774
  };
379
775
  };
776
+
380
777
  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
778
 
382
779
  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 root() {
47
+ const root = room.getStorageSnapshot();
48
+ if (root === null) {
49
+ throw new Error(errmsg);
50
+ }
51
+ return root;
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,28 @@ 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 useConnectionIds() {
146
+ return useOthers(connectionIdSelector, _client.shallow);
147
+ }
148
+ function useOthersWithData(itemSelector, itemIsEqual) {
149
+ const wrappedSelector = React2.useCallback(
150
+ (others) => others.map((other) => ({
151
+ connectionId: other.connectionId,
152
+ data: itemSelector(other)
153
+ })),
154
+ [itemSelector]
155
+ );
156
+ const wrappedIsEqual = React2.useCallback(
157
+ (a, b) => {
158
+ const eq = itemIsEqual != null ? itemIsEqual : Object.is;
159
+ return a.length === b.length && a.every((atuple, index) => {
160
+ const btuple = b[index];
161
+ return atuple.connectionId === btuple.connectionId && eq(atuple.data, btuple.data);
162
+ });
163
+ },
164
+ [itemIsEqual]
165
+ );
166
+ return useOthers(wrappedSelector, wrappedIsEqual);
140
167
  }
141
168
  const sentinel = Symbol();
142
169
  function useOther(connectionId, selector, isEqual) {
@@ -249,13 +276,7 @@ function createRoomContext(client) {
249
276
  const subscribe = room.events.storageDidLoad.subscribeOnce;
250
277
  const getSnapshot = room.getStorageSnapshot;
251
278
  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
- );
279
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
259
280
  }
260
281
  function useStorageRoot() {
261
282
  return [useMutableStorageRoot()];
@@ -271,21 +292,15 @@ function createRoomContext(client) {
271
292
  }
272
293
  function useCanUndo() {
273
294
  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;
295
+ const subscribe = room.events.history.subscribe;
296
+ const canUndo = room.history.canUndo;
297
+ return useSyncExternalStore(subscribe, canUndo, canUndo);
280
298
  }
281
299
  function useCanRedo() {
282
300
  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;
301
+ const subscribe = room.events.history.subscribe;
302
+ const canRedo = room.history.canRedo;
303
+ return useSyncExternalStore(subscribe, canRedo, canRedo);
289
304
  }
290
305
  function useBatch() {
291
306
  return useRoom().batch;
@@ -295,7 +310,7 @@ function createRoomContext(client) {
295
310
  const root = useMutableStorageRoot();
296
311
  const rerender = useRerender();
297
312
  React2.useEffect(() => {
298
- if (root == null) {
313
+ if (root === null) {
299
314
  return;
300
315
  }
301
316
  let liveValue = root.get(key);
@@ -325,7 +340,7 @@ function createRoomContext(client) {
325
340
  unsubscribeCrdt();
326
341
  };
327
342
  }, [root, room, key, rerender]);
328
- if (root == null) {
343
+ if (root === null) {
329
344
  return null;
330
345
  } else {
331
346
  return root.get(key);
@@ -390,27 +405,13 @@ function createRoomContext(client) {
390
405
  }
391
406
  function useMutation(callback, deps) {
392
407
  const room = useRoom();
393
- const root = useMutableStorageRoot();
394
- const setMyPresence = room.updatePresence;
395
408
  return React2.useMemo(
396
409
  () => {
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
- }
410
+ return (...args) => room.batch(
411
+ () => callback(makeMutationContext(room), ...args)
412
+ );
412
413
  },
413
- deps !== void 0 ? [root, room, setMyPresence, ...deps] : [root, room, setMyPresence, callback]
414
+ [room, ...deps]
414
415
  );
415
416
  }
416
417
  function useStorageSuspense(selector, isEqual) {
@@ -434,12 +435,9 @@ function createRoomContext(client) {
434
435
  isEqual
435
436
  );
436
437
  }
437
- function useOtherIdsSuspense(itemSelector, isEqual) {
438
+ function useOthersWithDataSuspense(itemSelector, itemIsEqual) {
438
439
  useSuspendUntilPresenceLoaded();
439
- return useOtherIds(
440
- itemSelector,
441
- isEqual
442
- );
440
+ return useOthersWithData(itemSelector, itemIsEqual);
443
441
  }
444
442
  function useOtherSuspense(connectionId, selector, isEqual) {
445
443
  useSuspendUntilPresenceLoaded();
@@ -454,39 +452,57 @@ function createRoomContext(client) {
454
452
  return useLegacyKey(key);
455
453
  }
456
454
  return {
455
+ RoomContext,
457
456
  RoomProvider,
457
+ useRoom,
458
458
  useBatch,
459
459
  useBroadcastEvent,
460
- useCanRedo,
461
- useCanUndo,
462
460
  useErrorListener,
463
461
  useEventListener,
464
462
  useHistory,
465
- useMyPresence,
466
- useOthers,
467
- useOtherIds,
468
- useOther,
463
+ useUndo,
469
464
  useRedo,
470
- useRoom,
471
- useSelf,
465
+ useCanRedo,
466
+ useCanUndo,
467
+ useList: useLegacyKey,
468
+ useMap: useLegacyKey,
469
+ useObject: useLegacyKey,
472
470
  useStorageRoot,
473
471
  useStorage,
474
- useUndo,
472
+ useSelf,
473
+ useMyPresence,
475
474
  useUpdateMyPresence,
475
+ useOthers,
476
+ useOthersWithData,
477
+ useConnectionIds,
478
+ useOther,
476
479
  useMutation,
477
- useList: useLegacyKey,
478
- useMap: useLegacyKey,
479
- useObject: useLegacyKey,
480
- RoomContext,
481
480
  suspense: {
481
+ RoomContext,
482
+ RoomProvider,
483
+ useRoom,
484
+ useBatch,
485
+ useBroadcastEvent,
486
+ useErrorListener,
487
+ useEventListener,
488
+ useHistory,
489
+ useUndo,
490
+ useRedo,
491
+ useCanRedo,
492
+ useCanUndo,
493
+ useList: useLegacyKeySuspense,
494
+ useMap: useLegacyKeySuspense,
495
+ useObject: useLegacyKeySuspense,
496
+ useStorageRoot,
482
497
  useStorage: useStorageSuspense,
483
498
  useSelf: useSelfSuspense,
499
+ useMyPresence,
500
+ useUpdateMyPresence,
484
501
  useOthers: useOthersSuspense,
485
- useOtherIds: useOtherIdsSuspense,
502
+ useOthersWithData: useOthersWithDataSuspense,
503
+ useConnectionIds,
486
504
  useOther: useOtherSuspense,
487
- useList: useLegacyKeySuspense,
488
- useMap: useLegacyKeySuspense,
489
- useObject: useLegacyKeySuspense
505
+ useMutation
490
506
  }
491
507
  };
492
508
  }
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-beta3",
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-beta3",
31
31
  "react": "^16.14.0 || ^17 || ^18"
32
32
  },
33
33
  "repository": {