@liveblocks/react 0.18.0-beta0 → 0.18.0-beta1

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
- 11a0666afa7ed3e5356b3c304466109fb37d8b87
1
+ 82a138d642402a9a15f754de8f42105c6724d0b0
package/index.d.ts CHANGED
@@ -1,7 +1,31 @@
1
+ import * as React from 'react';
2
+ import { ReactElement, ReactNode } from 'react';
1
3
  import { JsonObject, LsonObject, LiveObject, BaseUserMeta, Json, Client, Room, BroadcastOptions, History, Others, User } from '@liveblocks/client';
2
- export { Json, JsonObject } from '@liveblocks/client';
4
+ export { Json, JsonObject, shallow } from '@liveblocks/client';
3
5
  import { ToImmutable, Resolve, RoomInitializers } from '@liveblocks/client/internal';
4
- import * as React from 'react';
6
+
7
+ declare type Props = {
8
+ fallback: NonNullable<ReactNode> | null;
9
+ children: () => ReactNode | undefined;
10
+ };
11
+ /**
12
+ * Almost like a normal <Suspense> component, except that for server-side
13
+ * renders, the fallback will be used.
14
+ *
15
+ * The child props will have to be provided in a function, i.e. change:
16
+ *
17
+ * <Suspense fallback={<Loading />}>
18
+ * <MyRealComponent a={1} />
19
+ * </Suspense>
20
+ *
21
+ * To:
22
+ *
23
+ * <ClientSideSuspense fallback={<Loading />}>
24
+ * {() => <MyRealComponent a={1} />}
25
+ * </ClientSideSuspense>
26
+ *
27
+ */
28
+ declare function ClientSideSuspense(props: Props): ReactElement;
5
29
 
6
30
  /**
7
31
  * For any function type, returns a similar function type, but without the
@@ -210,6 +234,70 @@ declare type RoomContextBundle<TPresence extends JsonObject, TStorage extends Ls
210
234
  *
211
235
  */
212
236
  useOthers<T>(selector: (others: Others<TPresence, TUserMeta>) => T, isEqual?: (a: T, b: T) => boolean): T;
237
+ /**
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.
242
+ *
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.
245
+ *
246
+ * @example
247
+ * const ids = useOtherIds();
248
+ * // ^^^ number[]
249
+ */
250
+ useOtherIds(): readonly number[];
251
+ /**
252
+ * Related to useOthers(), but optimized for selecting only "subsets" of
253
+ * others. This is useful for performance reasons in particular, because
254
+ * selecting only a subset of users also means limiting the number of
255
+ * re-renders that will be triggered.
256
+ *
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
+ * @example
261
+ * const avatars = useOtherIds(user => user.info.avatar);
262
+ * // ^^^^^^^
263
+ * // { connectionId: number; data: string }[]
264
+ *
265
+ * The selector function you pass to useOtherIds() is called an "item
266
+ * 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.
268
+ *
269
+ * For example, to select multiple properties:
270
+ *
271
+ * @example
272
+ * const avatarsAndCursors = useOtherIds(
273
+ * user => [u.info.avatar, u.presence.cursor],
274
+ * shallow, // ❗️
275
+ * );
276
+ */
277
+ useOtherIds<T>(itemSelector: (other: User<TPresence, TUserMeta>) => T, isEqual?: (a: T, b: T) => boolean): readonly {
278
+ readonly connectionId: number;
279
+ readonly data: T;
280
+ }[];
281
+ /**
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.
285
+ *
286
+ * @example
287
+ * // Returns full user and re-renders whenever anything on the user changes
288
+ * const secondUser = useOther(2);
289
+ */
290
+ useOther(connectionId: number): User<TPresence, TUserMeta>;
291
+ /**
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.
295
+ *
296
+ * @example
297
+ * // Returns only the selected values re-renders whenever that selection changes)
298
+ * const { x, y } = useOther(2, user => user.presence.cursor);
299
+ */
300
+ useOther<T>(connectionId: number, selector: (other: User<TPresence, TUserMeta>) => T, isEqual?: (a: T, b: T) => boolean): T;
213
301
  /**
214
302
  * Returns the Room of the nearest RoomProvider above in the React component
215
303
  * tree.
@@ -277,8 +365,18 @@ declare type RoomContextBundle<TPresence extends JsonObject, TStorage extends Ls
277
365
  useSelf<T>(selector: (me: User<TPresence, TUserMeta>) => T, isEqual?: (a: T, b: T) => boolean): T;
278
366
  useOthers(): Others<TPresence, TUserMeta>;
279
367
  useOthers<T>(selector: (others: Others<TPresence, TUserMeta>) => T, isEqual?: (a: T, b: T) => boolean): T;
368
+ useOtherIds(): readonly number[];
369
+ useOtherIds<T>(itemSelector: (other: User<TPresence, TUserMeta>) => T, isEqual?: (a: T, b: T) => boolean): readonly {
370
+ readonly connectionId: number;
371
+ readonly data: T;
372
+ }[];
373
+ useOther(connectionId: number): User<TPresence, TUserMeta>;
374
+ useOther<T>(connectionId: number, selector: (other: User<TPresence, TUserMeta>) => T, isEqual?: (a: T, b: T) => boolean): T;
375
+ useList<TKey extends Extract<keyof TStorage, string>>(key: TKey): TStorage[TKey];
376
+ useMap<TKey extends Extract<keyof TStorage, string>>(key: TKey): TStorage[TKey];
377
+ useObject<TKey extends Extract<keyof TStorage, string>>(key: TKey): TStorage[TKey];
280
378
  };
281
379
  };
282
380
  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>;
283
381
 
284
- export { MutationContext, createRoomContext };
382
+ export { ClientSideSuspense, MutationContext, createRoomContext };
package/index.js CHANGED
@@ -1,6 +1,19 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } }// src/factory.tsx
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } }// src/ClientSideSuspense.tsx
2
+ var _react = require('react'); var React = _interopRequireWildcard(_react); var React2 = _interopRequireWildcard(_react);
3
+ function ClientSideSuspense(props) {
4
+ const [mounted, setMounted] = React.useState(false);
5
+ React.useEffect(() => {
6
+ setMounted(true);
7
+ }, []);
8
+ return /* @__PURE__ */ React.createElement(React.Suspense, {
9
+ fallback: props.fallback
10
+ }, mounted ? props.children() : props.fallback);
11
+ }
12
+
13
+ // src/factory.tsx
14
+ var _client = require('@liveblocks/client');
2
15
  var _internal = require('@liveblocks/client/internal');
3
- var _react = require('react'); var React = _interopRequireWildcard(_react);
16
+
4
17
  var _withselector = require('use-sync-external-store/shim/with-selector');
5
18
 
6
19
  // src/hooks.ts
@@ -20,21 +33,12 @@ function useInitial(value) {
20
33
  var noop = () => {
21
34
  };
22
35
  var identity = (x) => x;
23
- var EMPTY_OTHERS = [];
24
- Object.defineProperty(EMPTY_OTHERS, "count", {
25
- value: 0,
26
- enumerable: false
27
- });
28
- Object.defineProperty(EMPTY_OTHERS, "toArray", {
29
- value: () => EMPTY_OTHERS,
30
- enumerable: false
31
- });
32
- _internal.freeze.call(void 0, EMPTY_OTHERS);
36
+ var EMPTY_OTHERS = _internal.asArrayWithLegacyMethods.call(void 0, []);
33
37
  function getEmptyOthers() {
34
38
  return EMPTY_OTHERS;
35
39
  }
36
40
  function createRoomContext(client) {
37
- const RoomContext = React.createContext(null);
41
+ const RoomContext = React2.createContext(null);
38
42
  function RoomProvider(props) {
39
43
  const { id: roomId, initialPresence, initialStorage } = props;
40
44
  if (process.env.NODE_ENV !== "production") {
@@ -51,14 +55,14 @@ function createRoomContext(client) {
51
55
  initialPresence,
52
56
  initialStorage
53
57
  });
54
- const [room, setRoom] = React.useState(
58
+ const [room, setRoom] = React2.useState(
55
59
  () => client.enter(roomId, {
56
60
  initialPresence,
57
61
  initialStorage,
58
62
  DO_NOT_USE_withoutConnecting: typeof window === "undefined"
59
63
  })
60
64
  );
61
- React.useEffect(() => {
65
+ React2.useEffect(() => {
62
66
  setRoom(
63
67
  client.enter(roomId, {
64
68
  initialPresence: frozen.initialPresence,
@@ -70,12 +74,15 @@ function createRoomContext(client) {
70
74
  client.leave(roomId);
71
75
  };
72
76
  }, [roomId, frozen]);
73
- return /* @__PURE__ */ React.createElement(RoomContext.Provider, {
77
+ return /* @__PURE__ */ React2.createElement(RoomContext.Provider, {
74
78
  value: room
75
79
  }, props.children);
76
80
  }
81
+ function connectionIdSelector(others) {
82
+ return others.map((user) => user.connectionId);
83
+ }
77
84
  function useRoom() {
78
- const room = React.useContext(RoomContext);
85
+ const room = React2.useContext(RoomContext);
79
86
  if (room == null) {
80
87
  throw new Error("RoomProvider is missing from the react tree");
81
88
  }
@@ -85,11 +92,8 @@ function createRoomContext(client) {
85
92
  const room = useRoom();
86
93
  const presence = room.getPresence();
87
94
  const rerender = useRerender();
88
- React.useEffect(() => room.events.me.subscribe(rerender), [room, rerender]);
89
- const setPresence = React.useCallback(
90
- (patch, options) => room.updatePresence(patch, options),
91
- [room]
92
- );
95
+ const setPresence = room.updatePresence;
96
+ React2.useEffect(() => room.events.me.subscribe(rerender), [room, rerender]);
93
97
  return [presence, setPresence];
94
98
  }
95
99
  function useUpdateMyPresence() {
@@ -98,10 +102,7 @@ function createRoomContext(client) {
98
102
  function useOthers(selector, isEqual) {
99
103
  const room = useRoom();
100
104
  const subscribe = room.events.others.subscribe;
101
- const getSnapshot = React.useCallback(
102
- () => room.getOthers(),
103
- [room]
104
- );
105
+ const getSnapshot = room.getOthers;
105
106
  const getServerSnapshot = getEmptyOthers;
106
107
  return _withselector.useSyncExternalStoreWithSelector.call(void 0,
107
108
  subscribe,
@@ -111,9 +112,80 @@ function createRoomContext(client) {
111
112
  isEqual
112
113
  );
113
114
  }
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
+ }
140
+ }
141
+ const sentinel = Symbol();
142
+ function useOther(connectionId, selector, isEqual) {
143
+ const _useCallback = React2.useCallback;
144
+ const _useOthers = useOthers;
145
+ if (selector === void 0) {
146
+ const selector2 = _useCallback(
147
+ (others) => others.find((other2) => other2.connectionId === connectionId),
148
+ [connectionId]
149
+ );
150
+ const other = _useOthers(selector2, _client.shallow);
151
+ if (other === void 0) {
152
+ throw new Error(
153
+ `No such other user with connection id ${connectionId} exists`
154
+ );
155
+ }
156
+ return other;
157
+ } else {
158
+ const wrappedSelector = _useCallback(
159
+ (others) => {
160
+ const other2 = others.find(
161
+ (other3) => other3.connectionId === connectionId
162
+ );
163
+ return other2 !== void 0 ? selector(other2) : sentinel;
164
+ },
165
+ [connectionId, selector]
166
+ );
167
+ const wrappedIsEqual = _useCallback(
168
+ (a, b) => {
169
+ if (a === sentinel || b === sentinel) {
170
+ return a === b;
171
+ }
172
+ const eq = isEqual != null ? isEqual : Object.is;
173
+ return eq(a, b);
174
+ },
175
+ [isEqual]
176
+ );
177
+ const other = _useOthers(wrappedSelector, wrappedIsEqual);
178
+ if (other === sentinel) {
179
+ throw new Error(
180
+ `No such other user with connection id ${connectionId} exists`
181
+ );
182
+ }
183
+ return other;
184
+ }
185
+ }
114
186
  function useBroadcastEvent() {
115
187
  const room = useRoom();
116
- return React.useCallback(
188
+ return React2.useCallback(
117
189
  (event, options = { shouldQueueEventIfNotReady: false }) => {
118
190
  room.broadcastEvent(event, options);
119
191
  },
@@ -122,22 +194,22 @@ function createRoomContext(client) {
122
194
  }
123
195
  function useErrorListener(callback) {
124
196
  const room = useRoom();
125
- const savedCallback = React.useRef(callback);
126
- React.useEffect(() => {
197
+ const savedCallback = React2.useRef(callback);
198
+ React2.useEffect(() => {
127
199
  savedCallback.current = callback;
128
200
  });
129
- React.useEffect(
201
+ React2.useEffect(
130
202
  () => room.events.error.subscribe((e) => savedCallback.current(e)),
131
203
  [room]
132
204
  );
133
205
  }
134
206
  function useEventListener(callback) {
135
207
  const room = useRoom();
136
- const savedCallback = React.useRef(callback);
137
- React.useEffect(() => {
208
+ const savedCallback = React2.useRef(callback);
209
+ React2.useEffect(() => {
138
210
  savedCallback.current = callback;
139
211
  });
140
- React.useEffect(() => {
212
+ React2.useEffect(() => {
141
213
  const listener = (eventData) => {
142
214
  savedCallback.current(eventData);
143
215
  };
@@ -146,7 +218,7 @@ function createRoomContext(client) {
146
218
  }
147
219
  function useSelf(maybeSelector, isEqual) {
148
220
  const room = useRoom();
149
- const subscribe = React.useCallback(
221
+ const subscribe = React2.useCallback(
150
222
  (onChange) => {
151
223
  const unsub1 = room.events.me.subscribe(onChange);
152
224
  const unsub2 = room.events.connection.subscribe(onChange);
@@ -159,11 +231,11 @@ function createRoomContext(client) {
159
231
  );
160
232
  const getSnapshot = room.getSelf;
161
233
  const selector = maybeSelector != null ? maybeSelector : identity;
162
- const wrappedSelector = React.useCallback(
234
+ const wrappedSelector = React2.useCallback(
163
235
  (me) => me !== null ? selector(me) : null,
164
236
  [selector]
165
237
  );
166
- const getServerSnapshot = React.useCallback(() => null, []);
238
+ const getServerSnapshot = React2.useCallback(() => null, []);
167
239
  return _withselector.useSyncExternalStoreWithSelector.call(void 0,
168
240
  subscribe,
169
241
  getSnapshot,
@@ -176,7 +248,7 @@ function createRoomContext(client) {
176
248
  const room = useRoom();
177
249
  const subscribe = room.events.storageDidLoad.subscribeOnce;
178
250
  const getSnapshot = room.getStorageSnapshot;
179
- const getServerSnapshot = React.useCallback(() => null, []);
251
+ const getServerSnapshot = React2.useCallback(() => null, []);
180
252
  const selector = identity;
181
253
  return _withselector.useSyncExternalStoreWithSelector.call(void 0,
182
254
  subscribe,
@@ -199,8 +271,8 @@ function createRoomContext(client) {
199
271
  }
200
272
  function useCanUndo() {
201
273
  const room = useRoom();
202
- const [canUndo, setCanUndo] = React.useState(room.history.canUndo);
203
- React.useEffect(
274
+ const [canUndo, setCanUndo] = React2.useState(room.history.canUndo);
275
+ React2.useEffect(
204
276
  () => room.events.history.subscribe(({ canUndo: canUndo2 }) => setCanUndo(canUndo2)),
205
277
  [room]
206
278
  );
@@ -208,8 +280,8 @@ function createRoomContext(client) {
208
280
  }
209
281
  function useCanRedo() {
210
282
  const room = useRoom();
211
- const [canRedo, setCanRedo] = React.useState(room.history.canRedo);
212
- React.useEffect(
283
+ const [canRedo, setCanRedo] = React2.useState(room.history.canRedo);
284
+ React2.useEffect(
213
285
  () => room.events.history.subscribe(({ canRedo: canRedo2 }) => setCanRedo(canRedo2)),
214
286
  [room]
215
287
  );
@@ -222,7 +294,7 @@ function createRoomContext(client) {
222
294
  const room = useRoom();
223
295
  const root = useMutableStorageRoot();
224
296
  const rerender = useRerender();
225
- React.useEffect(() => {
297
+ React2.useEffect(() => {
226
298
  if (root == null) {
227
299
  return;
228
300
  }
@@ -263,15 +335,15 @@ function createRoomContext(client) {
263
335
  const room = useRoom();
264
336
  const rootOrNull = useMutableStorageRoot();
265
337
  const selector = maybeSelector != null ? maybeSelector : identity;
266
- const wrappedSelector = React.useCallback(
338
+ const wrappedSelector = React2.useCallback(
267
339
  (rootOrNull2) => rootOrNull2 !== null ? selector(rootOrNull2) : null,
268
340
  [selector]
269
341
  );
270
- const subscribe = React.useCallback(
342
+ const subscribe = React2.useCallback(
271
343
  (onStoreChange) => rootOrNull !== null ? room.subscribe(rootOrNull, onStoreChange, { isDeep: true }) : noop,
272
344
  [room, rootOrNull]
273
345
  );
274
- const getSnapshot = React.useCallback(() => {
346
+ const getSnapshot = React2.useCallback(() => {
275
347
  if (rootOrNull === null) {
276
348
  return null;
277
349
  } else {
@@ -280,7 +352,7 @@ function createRoomContext(client) {
280
352
  return imm;
281
353
  }
282
354
  }, [rootOrNull]);
283
- const getServerSnapshot = React.useCallback(() => null, []);
355
+ const getServerSnapshot = React2.useCallback(() => null, []);
284
356
  return _withselector.useSyncExternalStoreWithSelector.call(void 0,
285
357
  subscribe,
286
358
  getSnapshot,
@@ -320,7 +392,7 @@ function createRoomContext(client) {
320
392
  const room = useRoom();
321
393
  const root = useMutableStorageRoot();
322
394
  const setMyPresence = room.updatePresence;
323
- return React.useMemo(
395
+ return React2.useMemo(
324
396
  () => {
325
397
  if (root !== null) {
326
398
  const mutationCtx = {
@@ -366,6 +438,25 @@ function createRoomContext(client) {
366
438
  isEqual
367
439
  );
368
440
  }
441
+ function useOtherIdsSuspense(itemSelector, isEqual) {
442
+ useSuspendUntilPresenceLoaded();
443
+ return useOtherIds(
444
+ itemSelector,
445
+ isEqual
446
+ );
447
+ }
448
+ function useOtherSuspense(connectionId, selector, isEqual) {
449
+ useSuspendUntilPresenceLoaded();
450
+ return useOther(
451
+ connectionId,
452
+ selector,
453
+ isEqual
454
+ );
455
+ }
456
+ function useLegacyKeySuspense(key) {
457
+ useSuspendUntilStorageLoaded();
458
+ return useLegacyKey(key);
459
+ }
369
460
  return {
370
461
  RoomProvider,
371
462
  useBatch,
@@ -377,6 +468,8 @@ function createRoomContext(client) {
377
468
  useHistory,
378
469
  useMyPresence,
379
470
  useOthers,
471
+ useOtherIds,
472
+ useOther,
380
473
  useRedo,
381
474
  useRoom,
382
475
  useSelf,
@@ -392,10 +485,20 @@ function createRoomContext(client) {
392
485
  suspense: {
393
486
  useStorage: useStorageSuspense,
394
487
  useSelf: useSelfSuspense,
395
- useOthers: useOthersSuspense
488
+ useOthers: useOthersSuspense,
489
+ useOtherIds: useOtherIdsSuspense,
490
+ useOther: useOtherSuspense,
491
+ useList: useLegacyKeySuspense,
492
+ useMap: useLegacyKeySuspense,
493
+ useObject: useLegacyKeySuspense
396
494
  }
397
495
  };
398
496
  }
399
497
 
498
+ // src/index.ts
499
+
500
+
501
+
502
+
400
503
 
401
- exports.createRoomContext = createRoomContext;
504
+ exports.ClientSideSuspense = ClientSideSuspense; exports.createRoomContext = createRoomContext; exports.shallow = _client.shallow;
package/index.mjs CHANGED
@@ -1,4 +1,6 @@
1
1
  import mod from "./index.js";
2
2
 
3
3
  export default mod;
4
+ export const ClientSideSuspense = mod.ClientSideSuspense;
4
5
  export const createRoomContext = mod.createRoomContext;
6
+ export const shallow = mod.shallow;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liveblocks/react",
3
- "version": "0.18.0-beta0",
3
+ "version": "0.18.0-beta1",
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-beta0",
30
+ "@liveblocks/client": "0.18.0-beta1",
31
31
  "react": "^16.14.0 || ^17 || ^18"
32
32
  },
33
33
  "repository": {