Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
## vNEXT (not yet released)

### `@liveblocks/react`

- Add new hook `useMutableStorage()` to get direct access to the mutable Storage
root. See
[docs](https://liveblocks.io/docs/api-reference/liveblocks-react#useMutableStorage).

## v3.24.0

This release introduces `LiveText` (beta), a collaborative rich-text data
Expand Down
30 changes: 7 additions & 23 deletions docs/pages/api-reference/liveblocks-codemirror.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -81,35 +81,26 @@ export default function App() {
}
```

Attach the plugins after Storage has loaded. Create the editor with the
`LiveText` content and both plugins in the initial extensions:
Use
[`useMutableStorage`](/docs/api-reference/liveblocks-react#useMutableStorage) to
get the `LiveText` once Storage has loaded, then create the editor with its
content and both plugins in the initial extensions:

```tsx file="Editor.tsx"
"use client";

import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
import { useEffect, useRef } from "react";
import { EditorView } from "@codemirror/view";
import { EditorState } from "@codemirror/state";
import type { LiveText, Room } from "@liveblocks/client";
import {
createLiveblocksPresencePlugin,
createLiveblocksSyncPlugin,
} from "@liveblocks/codemirror";
import { useRoom } from "@liveblocks/react/suspense";
import { useMutableStorage, useRoom } from "@liveblocks/react/suspense";

export function Editor() {
const room = useRoom();
const root = useRoot(room);

if (root == null) {
return <div>Loading…</div>;
}

return <EditorInner text={root.get("document")} />;
}

function EditorInner({ text }: { text: LiveText }) {
const room = useRoom();
const text = useMutableStorage().get("document");
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
Expand All @@ -135,13 +126,6 @@ function EditorInner({ text }: { text: LiveText }) {

return <div ref={containerRef} className="editor" />;
}

function useRoot(room: Room) {
const subscribe = room.events.storageDidLoad.subscribeOnce;
const getSnapshot = room.getStorageOrNull;
const getServerSnapshot = useCallback(() => null, []);
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}
```

Add styles for remote carets and selections. This package does not ship a
Expand Down
26 changes: 6 additions & 20 deletions docs/pages/api-reference/liveblocks-lexical.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,16 @@ export default function App() {
}
```

Wait for Storage to load, then nest
Use
[`useMutableStorage`](/docs/api-reference/liveblocks-react#useMutableStorage) to
get the document root once Storage has loaded, then nest
[`LiveblocksCollaborationPlugin`](#LiveblocksCollaborationPlugin) inside
[`LexicalComposer`](https://lexical.dev/docs/react/plugins). Optionally add
[`RemoteCursorsPlugin`](#RemoteCursorsPlugin) as a child to show remote carets:

```tsx file="Editor.tsx"
"use client";

import { useCallback, useSyncExternalStore } from "react";
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
Expand All @@ -128,19 +129,11 @@ import {
LiveblocksCollaborationPlugin,
RemoteCursorsPlugin,
} from "@liveblocks/lexical";
import type { Room } from "@liveblocks/client";
import { useRoom } from "@liveblocks/react/suspense";
import { useMutableStorage } from "@liveblocks/react/suspense";
import "@liveblocks/lexical/styles.css";

export function Editor() {
const room = useRoom();
const root = useRoot(room);

if (root === null) {
return <div>Loading…</div>;
}

const document = root.get("document");
const document = useMutableStorage().get("document");

return (
<LexicalComposer
Expand All @@ -163,13 +156,6 @@ export function Editor() {
</LexicalComposer>
);
}

function useRoot(room: Room) {
const subscribe = room.events.storageDidLoad.subscribeOnce;
const getSnapshot = room.getStorageOrNull;
const getServerSnapshot = useCallback(() => null, []);
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}
```

Import the package stylesheet so remote carets and selections are visible:
Expand Down Expand Up @@ -199,7 +185,7 @@ import { LiveblocksCollaborationPlugin } from "@liveblocks/lexical";
<PropertiesList title="Props">
<PropertiesListItem name="root" type="LiveRootNode" required>
The Storage root document for the editor. Typically
`root.get("document")` after Storage has loaded.
`useMutableStorage().get("document")`.
</PropertiesListItem>
<PropertiesListItem name="children" type="ReactNode">
Optional children. Place [`RemoteCursorsPlugin`](#RemoteCursorsPlugin) here
Expand Down
67 changes: 65 additions & 2 deletions docs/pages/api-reference/liveblocks-react.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3937,14 +3937,14 @@ needs. This will avoid unnecessary rerenders that happen with overselection.
In order to select one item from a LiveMap within the storage tree with the
`useStorage` method, you can use the example below:

```ts
```tsx showLineNumbers={false}
const key = "errands";
const myTodos = useStorage((root) => root.todoMap.get(key));
```

In order to query a LiveMap, and filter for specific values:

```ts
```tsx showLineNumbers={false}
const myTodos = useStorage(
root => Array.from(root.todoMap.values()).filter(...),
shallow,
Expand All @@ -3971,6 +3971,69 @@ const myTodos = useStorage(
</PropertiesListItem>
</PropertiesList>

### useMutableStorage [@badge=RoomProvider]

Returns the current room's _mutable_ Storage root. Always a
[`LiveObject`](/docs/api-reference/liveblocks-client#LiveObject).

```tsx showLineNumbers={false}
import { useMutableStorage } from "@liveblocks/react/suspense";

function Editor() {
const root = useMutableStorage();
const liveText = root.get("document");

// Pass `liveText` to your editor binding
// ...
}
```

Unlike [`useStorage`][], this hook is **not reactive**. Your component rerenders
only once, when Storage has finished loading, and never again when the contents
of the tree change. That’s the point: the `LiveText` you hand to an editor stays
referentially stable, so your editor isn’t torn down on every keystroke.

```tsx showLineNumbers={false}
// Rerenders on every keystroke
const text = useStorage((root) => root.document);

// Rerenders only once, after Storage has loaded
const liveText = useMutableStorage().get("document");
```

<Banner title="You are responsible for batching">

If you make changes to Storage via `useMutableStorage`, you’re also responsible
for batching mutations when you make them. This is unlike
[`useMutation`](/docs/api-reference/liveblocks-react#useMutation), which
automatically does that for you. You should always wrap related changes in
[`Room.batch`](/docs/api-reference/liveblocks-client#Room.batch) yourself, so
they’re sent as one update and undoable as a single step.

```tsx showLineNumbers={false}
const room = useRoom();
const root = useMutableStorage();

room.batch(() => {
root.get("settings").set("theme", "dark");
root.get("settings").set("fontSize", 14);
});
```

</Banner>

The non-Suspense version returns `null` while Storage is still loading. The
[Suspense version][] suspends instead, and always returns the root.

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
<PropertiesListItem name="root" type="LiveObject<TStorage> | null">
The mutable Storage root. Returns `null` while Storage is still loading (in
the non-Suspense version).
</PropertiesListItem>
</PropertiesList>

### useUploadFile [@badge=RoomProvider]

Returns a function that uploads a file to the current room. The function
Expand Down
24 changes: 3 additions & 21 deletions docs/pages/get-started/nextjs-codemirror.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -158,29 +158,18 @@ collaboration to your Next.js application using the APIs from the
```tsx file="app/Editor.tsx"
"use client";

import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
import { useEffect, useRef } from "react";
import { EditorView } from "@codemirror/view";
import { EditorState } from "@codemirror/state";
import type { LiveText, Room } from "@liveblocks/client";
import {
createLiveblocksPresencePlugin,
createLiveblocksSyncPlugin,
} from "@liveblocks/codemirror";
import { useRoom } from "@liveblocks/react/suspense";
import { useMutableStorage, useRoom } from "@liveblocks/react/suspense";

export function Editor() {
const room = useRoom();
const root = useRoot(room);

if (root == null) {
return <div>Loading…</div>;
}

return <EditorInner text={root.get("document")} />;
}

function EditorInner({ text }: { text: LiveText }) {
const room = useRoom();
const text = useMutableStorage().get("document");
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
Expand All @@ -206,13 +195,6 @@ collaboration to your Next.js application using the APIs from the

return <div ref={containerRef} className="editor" />;
}

function useRoot(room: Room) {
const subscribe = room.events.storageDidLoad.subscribeOnce;
const getSnapshot = room.getStorageOrNull;
const getServerSnapshot = useCallback(() => null, []);
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}
```

</StepContent>
Expand Down
20 changes: 2 additions & 18 deletions docs/pages/get-started/nextjs-lexical-storage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,6 @@ instead.
```tsx file="app/Editor.tsx"
"use client";

import { useCallback, useSyncExternalStore } from "react";
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
Expand All @@ -197,19 +196,11 @@ instead.
LiveblocksCollaborationPlugin,
RemoteCursorsPlugin,
} from "@liveblocks/lexical";
import type { Room } from "@liveblocks/client";
import { useRoom } from "@liveblocks/react/suspense";
import { useMutableStorage } from "@liveblocks/react/suspense";
import "@liveblocks/lexical/styles.css";

export function Editor() {
const room = useRoom();
const root = useRoot(room);

if (root === null) {
return <div>Loading…</div>;
}

const document = root.get("document");
const document = useMutableStorage().get("document");

return (
<LexicalComposer
Expand All @@ -232,13 +223,6 @@ instead.
</LexicalComposer>
);
}

function useRoot(room: Room) {
const subscribe = room.events.storageDidLoad.subscribeOnce;
const getSnapshot = room.getStorageOrNull;
const getServerSnapshot = useCallback(() => null, []);
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}
```

</StepContent>
Expand Down
24 changes: 3 additions & 21 deletions docs/pages/get-started/react-codemirror.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -161,29 +161,18 @@ collaboration to your React application using the APIs from the
```tsx file="Editor.tsx"
"use client";

import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
import { useEffect, useRef } from "react";
import { EditorView } from "@codemirror/view";
import { EditorState } from "@codemirror/state";
import type { LiveText, Room } from "@liveblocks/client";
import {
createLiveblocksPresencePlugin,
createLiveblocksSyncPlugin,
} from "@liveblocks/codemirror";
import { useRoom } from "@liveblocks/react/suspense";
import { useMutableStorage, useRoom } from "@liveblocks/react/suspense";

export function Editor() {
const room = useRoom();
const root = useRoot(room);

if (root == null) {
return <div>Loading…</div>;
}

return <EditorInner text={root.get("document")} />;
}

function EditorInner({ text }: { text: LiveText }) {
const room = useRoom();
const text = useMutableStorage().get("document");
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
Expand All @@ -209,13 +198,6 @@ collaboration to your React application using the APIs from the

return <div ref={containerRef} className="editor" />;
}

function useRoot(room: Room) {
const subscribe = room.events.storageDidLoad.subscribeOnce;
const getSnapshot = room.getStorageOrNull;
const getServerSnapshot = useCallback(() => null, []);
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}
```

</StepContent>
Expand Down
Loading
Loading