Upgrading - Upgrading to 0.17

$npm install @liveblocks/client@0.17 @liveblocks/react@0.17

With the release of 0.17 we’re making a big investment in the stability and reliability of Liveblocks. Our long term goal is to empower you to write and help evolve your apps in the best way possible, and at enterprise scale. It already was easy to get started with Liveblocks, and with these changes we want to make evolving your app just as easy.

The first step towards this goal is to take TypeScript support to the next level for @liveblocks/client and @liveblocks/react (but our other packages will soon follow suit). This will help you write code with confidence and catch bugs as soon as possible in the development process. We have strictened our type definitions to be more accurate and will recommend some new usage patterns, so it will be easier for you to create bug free collaborative apps.

To upgrade @liveblocks/client and @liveblocks/react, run the following command.

$npm install @liveblocks/client@0.17 @liveblocks/react@0.17

With these changes, we’re clearing the path to enable schema validation per room, automatic data migrations, more powerful data selector APIs, and other enterprise-level features.

Let’s dive in and take a look!

Changes in @liveblocks/react

Lifting up your state to the room level

With 0.16, it was possible to initialize a storage key with useObject, useList, useMap.

Even if handy, we realized that it introduced confusion and unpredictable behavior for most users. Imagine a scenario where you have two components initializing the same storage key.

function ComponentA() {  const author = useObject("author", {    firstName: "Ada",    lastName: "Lovelace",  });
/* ... */}
function ComponentB() { const author = useObject("author", { firstName: "Margaret", lastName: "Hamilton", });
/* ... */}

Depending on which component renders first, author will be Margaret Hamilton or Ada Lovelace. To make this more predictable, we’re deprecating this and recommend initializing the storage at the RoomProvider level.

Before ❌

function ComponentA() {  const author = useObject(    "author",
// ⚠️ Don’t initialize your data here anymore! { firstName: "Ada", lastName: "Lovelace" } );
/* ... */}

After ✅

import { LiveObject } from "@liveblocks/client";
function Root() { // Instead, initialize it at the RoomProvider level to remove all ambiguity const initialStorage = { author: new LiveObject({ firstName: "Ada", lastName: "Lovelace", }), };
return ( <RoomProvider id="my-room-id" initialStorage={initialStorage}> <ComponentA /> </RoomProvider> );}
function ComponentA() { const author = useObject("author");
/* ... */}

If you run into issues with these new patterns and you need help, please let us know. We’re here to help!

A better way to annotate your own types

In 0.16, most of our hooks accepted generic parameters that let you explicitly provide your own types. For example:

import { useMyPresence, RoomProvider } from "@liveblocks/react";
type Presence = { cursor: { x: number; y: number };};
function Root() { return ( <RoomProvider id="my-room-id" initialPresence={{ cursor: { x: 100, y: 100 } }} > <Component /> </RoomProvider> );}
function Component() { const [myPresence] = useMyPresence<Presence>();
const cursor = myPresence.cursor; // Valid
/* ... */}

One issue with this API was that there was no good way to make sure that RoomProvider.initialPresence and useMyPresence types remain synchronized, as there was no inherent connection between these.

If we added a color property to the Presence type, it would still be missing from the initialPresence at the RoomProvider level, and TypeScript would not be able to catch that bug. The opposite would also fail; omitting the cursor property on the initialPresence would break at runtime but TypeScript would not be able to catch this issue for you!

Another issue is that there could be many places where you’d have to provide those extra type annotations.

With 0.17, we’re fixing all of this!

To do so, we’re introducing a new API called createRoomContext. It lets you type your RoomProvider and make sure that all your hooks types are synchronized with the RoomProvider. Besides this initial setup, you will no longer have to provide any type annotations elsewhere anymore.

import { createClient } from "@liveblocks/client";import { createRoomContext } from "@liveblocks/react";
const client = createClient({ /* client options */});
type Presence = { cursor: { x: number; y: number };};
// This is just to illustrate the API - read on for tips on where to put this!const { RoomProvider, useMyPresence } = createRoomContext<Presence>(client);
function Root() { return ( <RoomProvider id="my-room-id" initialPresence={{ cursor: { x: 100, y: 100 } }} > <Component /> </RoomProvider> );}
function Component() { const [myPresence] = useMyPresence();
// We can now be sure that cursor is a valid property without any generic typed param const cursor = myPresence.cursor;
/* ... */}

As you can see, createRoomContext optionally takes type parameters that let you specify the shape of your app’s data (by specifying your own Presence, Storage, UserMeta, Event types). Depending on the complexity of your app, you may only need to use one or more of these.

Take a look at these examples to better see how to use and configure it:

Recommended upgrade steps

To make this refactoring as easy as possible, follow the steps below.

Step 1 - Upgrade @liveblocks/client and @liveblocks/react

To upgrade @liveblocks/client and @liveblocks/react, run the following command.

$npm install @liveblocks/client@0.17 @liveblocks/react@0.17

Step 2 - Create a new file called liveblocks.config.ts where you will create your Liveblocks client, provider and hooks and re-export them.

liveblocks.config.ts
import { createClient } from "@liveblocks/client";import { createRoomContext } from "@liveblocks/react";
const client = createClient({ /* client options */});
// Presence represents the properties that will exist on every User in the Room// and that will automatically be kept in sync. Accessible through the// `user.presence` property. Must be JSON-serializable.type Presence = { // cursor: { x: number, y: number } | null, // ...};
// Optionally, Storage represents the shared document that persists in the// Room, even after all Users leave. Fields under Storage typically are// LiveList, LiveMap, LiveObject instances, for which updates are// automatically persisted and synced to all connected clients.type Storage = { // author: LiveObject<{ firstName: string, lastName: string }>, // ...};
// Optionally, UserMeta represents static/readonly metadata on each User, as// provided by your own custom auth backend (if used). Useful for data that// will not change during a session, like a User's name or avatar.// type UserMeta = {// id?: string, // Accessible through `user.id`// info?: Json, // Accessible through `user.info`// };
// Optionally, the type of custom events broadcasted and listened for in this// room. Must be JSON-serializable.// type RoomEvent = {};
export const { RoomProvider, useMyPresence, useObject, /* ...all the other hooks you’re using... */} = createRoomContext<Presence, Storage /* UserMeta, RoomEvent */>(client);

Step 3 - Replace all the direct hook imports from @liveblocks/react by your path to liveblocks.config.ts and remove all generic params.

Before ❌
import { useMyPresence, useOthers, useObject } from "@liveblocks/react";import { Author } from "./types";
type MyPresence = { cursor: { x: number; y: number } | null;};
function Component() { const author = useObject<Author>("author"); const [{ cursor }] = useMyPresence<MyPresence>(); const others = useOthers<Presence>();}
After ✅
import { useMyPresence, useOthers, useObject } from "./liveblocks.config";
function Component() { const author = useObject("author"); const [{ cursor }] = useMyPresence(); const others = useOthers();}

Step 4 - Remove your LiveblocksProvider at the top of your react tree. It’s not needed anymore!

Before ❌
import { createClient } from "@liveblocks/client";import { LiveblocksProvider, RoomProvider } from "@liveblocks/react";
const client = createClient({ /* ... */});
ReactDOM.render( <LiveblocksProvider client={client}> <RoomProvider id="my-room-id"> <App /> </RoomProvider> </LiveblocksProvider>, document.getElementById("root"));
After ✅
import { RoomProvider } from "./liveblocks.config";
ReactDOM.render( <RoomProvider id="my-room-id"> <App /> </RoomProvider>, document.getElementById("root"));

If you run into issues with these new patterns and you need help, please let us know. We’re here to help!

Changes in @liveblocks/client

Removed dangerous default type params

In 0.16, while LiveList, LiveMap, LiveObject were generics, they also took default type params, which made them a footgun. It was easy to accidentally use them in a way that would discard useful type information and hinder inference.

For example:

// ✅ Inferredlet list = new LiveList([1, 2, 3]);
// ✅ Explicitlet list: LiveList<number> = new LiveList([1, 2, 3]);
// ☢️ 0.16: Dangerous footgun: discards useful type information!// 🚫 0.17: No longer possiblelet list: LiveList = new LiveList([1, 2, 3]);

They now mimic their equivalent built-in TypeScript generics, so:

  • LiveList<T> is now just like Array<T>
  • LiveMap<K, V> is now just like Map<K, V>
  • LiveObject<{ a: number, b: string }> is now just like { a: number, b: string }

No longer import Presence

In 0.16, we exposed a Presence type that you could import, which was just an alias for “any JSON object”—not that useful! Importing Presence from Liveblocks made no sense. By definition, Presence is data owned and defined by your application after all.

You should no longer need to import this type.

// ❌ No longer need to _import_ Presenceimport type { Presence } from "@liveblocks/client";
client.enter<Presence>("myRoom");

Instead, just define it:

// ✅ Just define the shape your app needstype Presence = {  cursor: { x: number; y: number } | null;};
client.enter<Presence>("myRoom");

A better way to annotate your own types

We already talked about why, in React, we improved the way you can annotate your own app’s data, by annotating the types only once, at the “top” of your app.

For the same reason, we’re doing a similar thing in the client package.

import { createClient } from "@liveblocks/client";import { Author } from "./types";
const client = createClient({ /* client options */});
type Presence = { cursor: { x: number; y: number } | null;};
type Storage = { author: LiveObject<Author>;};
// ❌ In 0.16, you had to annotate each method separatelyconst room = client.enter("myRoom");const { root } = await room.getStorage<Storage>();const author = root.get<Author>("author");const me = room.getPresence<Presence>();const others = room.getOthers<Presence>();
// ✅ In 0.17, you can simply annotate it once, at the "top"const room = client.enter<Presence, Storage>("myRoom");const { root } = await room.getStorage();const author = root.get("author");const me = room.getPresence();const others = room.getOthers();

If you run into issues with these new patterns and you need help, please let us know. We’re here to help!