UpgradingUpgrading to 2.0

Liveblocks 2.0 is our second major release, and focuses on removing any rough edges and smoothening the developer experience. It makes Liveblocks simpler to set up for new users as well as for existing users. Our long term goal is to empower you to write and evolve your apps in the best way possible, and at enterprise scale.

Rationale

Liveblocks has always embraced the benefits of static typing with TypeScript. Roughly two years ago, we published 0.17, which introduced the pattern of calling createRoomContext() and returning a “bundle” of type-safe hooks that you could then use in your application. This pattern worked nicely because all the hooks were bound to your custom type definitions for Presence, Storage, etc. in one single place, and every Liveblocks API would deeply know about it. No need for manual type annotations anywhere else in your codebase.

Downsides to this approach

There were, however, a couple of downsides to this approach that have always kept itching:

  • Importing hooks from a local config file was a little bit awkward. It wasn’t an all too familiar pattern for most users.
  • Re-exporting the list of hooks took maintenance. If we added a new hook, you’d have to also re-export it manually.
  • Due to how TypeScript works, the type params you’d provide to createRoomContext() had a fixed ordering to them. If you wanted to specify only Presence and RoomEvent, you’d have to “skip” some type params, by doing createRoomContext<Presence, {}, {}, RoomEvent>() or similar.

Introducing Liveblocks 2.0

With 2.0, this all becomes a lot simpler. We’ve done a lot of internal refactoring to make this possible. We’ll dive in soon, but here is a sneak peek:

liveblocks.config.ts
// ❌ This pattern is no longer recommended unless you need multiple room typesexport const {  suspense: { RoomProvider, useRoom, useThread },} = createRoomContext<  MyPresence,  MyStorage,  MyUserMeta,  MyRoomEvent,  MyThreadMetadata>(client);
// ✅ Afterdeclare global { // These custom types are all optional, just define the ones you want/need interface Liveblocks { Presence: MyPresence; Storage: MyStorage; UserMeta: MyUserMeta; RoomEvent: MyRoomEvent; ThreadMetadata: MyThreadMetadata; }}
Component.tsx
// ❌ Beforeimport { RoomProvider, useRoom, useThreads } from "./liveblocks.config.ts";
// ✅ Afterimport { RoomProvider, useRoom, useThreads } from "@liveblocks/react/suspense";// orimport { RoomProvider, useRoom, useThreads } from "@liveblocks/react";

We’ll first go over all the breaking changes, then we’ll show you how to simplify your codebase.

How to upgrade

First of all, let’s upgrade all Liveblocks dependencies to their latest versions. The easiest way to do that is to run the following command:

$npx create-liveblocks-app@latest --upgrade

There are also some breaking changes in this update. Most users will not run into any of these, but there is a chance that some of these will affect your situation. Making the necessary code adjustments should however be easy. In many cases, we provide a codemod that makes the actual change for you, so you don’t have to do so manually.

Breaking change 1: renamed package

We’ve renamed our package @liveblocks/react-comments to @liveblocks/react-ui, because our library of pre-built UI components now contains more than just Comments-related components. Please adjust your imports.

Run the following codemod or manually make the changes:

$npx @liveblocks/codemod@latest react-comments-to-react-ui

This will change your imports like this:

// ❌ Beforeimport { Thread } from "@liveblocks/react-comments";
// ✅ Afterimport { Thread } from "@liveblocks/react-ui";

And also:

// ❌ Before<CommentsConfig />
// ✅ After<LiveblocksUIConfig />

Breaking change 2: renamed exports in our Node package

To avoid confusion with the newly introduced custom RoomInfo type, we’ve renamed the RoomInfo type in our @liveblocks/node package.

Run the following codemod or manually make the changes:

$npx @liveblocks/codemod@latest room-info-to-room-data

This will change:

// ❌ Beforeimport { RoomInfo } from "@liveblocks/node";const rooms: RoomInfo[] = [];
// ✅ Afterimport { RoomData } from "@liveblocks/node";const rooms: RoomData[] = [];

Breaking change 3: client methods from our Node package no longer take type params

In @liveblocks/node, none of the client methods, like liveblocks.getThread(), take type params any longer.

Make the following changes:

// ❌ Beforeawait liveblocks.createThread<MyThreadMetadata>();await liveblocks.editThreadMetadata<MyThreadMetadata>();await liveblocks.getThreads<MyThreadMetadata>();await liveblocks.getThread<MyThreadMetadata>();//                         ^^^^^^^^^^^^^^^^ No longer possible
// ✅ Afterawait liveblocks.createThread();await liveblocks.editThreadMetadata();await liveblocks.getThreads();await liveblocks.getThread();
// In liveblocks.config.tsdeclare global { interface Liveblocks { ThreadMetadata: MyThreadMetadata; }}

You should use global type augmentation instead. Please see “Simplifying your Liveblocks application” below for a lot more detail on this transition.

Breaking change 4: changed default export to named export in Yjs package

To make discoverability and refactorings easier, and to avoid confusion with the newly introduced LiveblocksProvider in our React package, we’re no longer using default exports, but named exports only.

Run the following codemod or manually make the changes:

$npx @liveblocks/codemod@latest remove-yjs-default-export

This will change your import (and its usage) like this:

// ❌ Beforeimport LiveblocksProvider from "@liveblocks/yjs";const yDoc = new Y.Doc();const yProvider = new LiveblocksProvider(room, yDoc);
// ✅ Afterimport { LiveblocksYjsProvider } from "@liveblocks/yjs";const yDoc = new Y.Doc();const yProvider = new LiveblocksYjsProvider(room, yDoc);

Breaking change 5: minor LiveList constructor change

The LiveList() constructor’s argument is no longer optional, because it causes unneeded but confusing type inference issues.

Run the following codemod or manually make the changes:

$npx @liveblocks/codemod@latest live-list-constructor

This will add an array to empty LiveList constructors:

// ❌ Beforeconst mylist = new LiveList();
// ✅ Afterconst mylist = new LiveList([]);

Breaking change 6: new webhook event types

The webhook event NotificationEvent’s type can represent multiple kinds of notifications: "thread", "textMention", and custom ones (e.g. "$myNotification").

If you were using properties only available on the "thread" kind (e.g. threadId), you will need to first check for the kind of notification before accessing them.

// ❌ Beforeconst threadId = event.data.threadId;
// ✅ Afterif (event.data.kind === "thread") { const threadId = event.data.threadId;}

Breaking change 7: removed deprecated APIs

All of the following APIs have been removed in 2.0, as they were deprecated multiple versions ago.

Affecting @liveblocks/client:

  • Client.enter() has been replaced by Client.enterRoom()
  • Client.leave() has been replaced by Client.enterRoom(), which returns a leave function
  • Client option fetchPolyfill, WebSocketPolyfill, are replaced by polyfills: { fetch, WebSocket }
  • Legacy option shouldInitiallyConnect is now renamed to autoConnect
  • Legacy connection status APIs, e.g. room.getConnectionState() and .subscribe("connection"). You can use .getStatus() or .subscribe("status") instead.
  • user.isReadOnly field is replaced by !user.canWrite (note the negation here)
  • The Others<P, U> type. Please change to readonly User<P, U>[].

Affecting @liveblocks/react:

  • The useMap, useList, and useObject hooks. These have been deprecated since the release of 0.18 (more than two years ago). Please see the 0.18 upgrade guide for tips on how to rewrite these hooks to useStorage.
  • The second argument options to createRoomContext(client, options) has been removed. These options have been moved to the client.

Affecting @liveblocks/node:

  • Remove legacy authorize method from @liveblocks/node. Please refer to the 1.2 upgrade guide to learn how to upgrade your auth endpoint.

Affecting @liveblocks/redux and @liveblocks/zustand:

  • Legacy aliased exports in Zustand/Redux packages are removed

Simplifying your Liveblocks application (optional)

If you have dealt with the breaking changes above, or concluded they they don’t apply to your situation, we can go a step further and really simplify your Liveblocks setup.

Step 1: Use the new global Liveblocks custom types

Go to your liveblocks.config.ts, find your createRoomContext() call. Now decide which codemod to run.

Option 1: You are using Suspense hooks. (const { suspense: { useRoom, ... } })

$npx @liveblocks/codemod@latest remove-liveblocks-config-contexts --suspense

Option 2: You are using classic hooks. (const { useRoom, ... })

$npx @liveblocks/codemod@latest remove-liveblocks-config-contexts

Running either of these will make the following changes.

// ❌ Beforeexport const {  ...} = createRoomContext<MyPresence, MyStorage, MyUserMeta, MyRoomEvent, MyThreadMetadata>(client);
// ✅ Afterdeclare global {  interface Liveblocks {    Presence: MyPresence;    Storage: MyStorage;    UserMeta: MyUserMeta;    RoomEvent: MyRoomEvent;    ThreadMetadata: MyThreadMetadata;  }}

Secondly, it will change all imports in your code base to import the hooks directly instead:

// ❌ Beforeimport { RoomProvider, useRoom, ... } from "./liveblocks.config.ts";
// ✅ Afterimport { RoomProvider, useRoom, ... } from "@liveblocks/react/suspense";  // Option 1import { RoomProvider, useRoom, ... } from "@liveblocks/react";           // Option 2

Step 2: Set up a LiveblocksProvider

At this point, there should not be any new TypeScript issues. However, running the code will not yet work. This is because previously the hooks were bound to the client instance by passing it to the createRoomContext() factory, which we now removed. When using the global types, we’ll have to provide the Liveblocks client otherwise.

The way to do it is to use a pretty standard React provider. Make the following change:

liveblocks.config.ts
// ❌ Before, you no longer have to use createClient()import { createClient } from "@liveblocks/client";
const client = createClient({ authEndpoint: "/api/liveblocks-auth", throttle: 16, /* etc */);
layout.tsx
// ✅ After"use client";
import { LiveblocksProvider } from "@liveblocks/react";
export function Layout({ children }) { return ( // Move options here, client will be created for you <LiveblocksProvider authEndpoint="/api/liveblocks-auth" throttle={16} /* etc */ > {children} </LiveblocksProvider> );}

If you were exporting the client instance before and have components that directly accessed it before, you can now obtain a reference to the client instance that the LiveblocksProvider creates for you using the useClient hook:

// ❌ Beforeimport { client } from "./liveblocks.config";
function MyComponent() { doSomethingWith(client);}
// ✅ Afterimport { useClient } from "@liveblocks/react"; // orimport { useClient } from "@liveblocks/react/suspense";
function MyComponent() { const client = useClient(); doSomethingWith(client);}

Step 3: Optional cleanup of type params

If you also exported your Presence, Storage, etc types from liveblocks.config.ts before, you no longer have to. The main reason to export these before was to use them in helper functions that used some of the Liveblocks types, like User, or Room.

$npx @liveblocks/codemod@latest remove-unneeded-type-params

For example:

import type { Room, User } from "@liveblocks/client";import type { MyPresence, MyStorage } from "./liveblocks.config";
// ❌ Beforefunction isAdult(user: User<MyPresence, MyUserMeta>) { // ^^^^^^^^^^^^^^^^^^^^^^ return user.info.age >= 18; // ^^^ Coming from MyUserMeta}
// ❌ Beforefunction doSomethingWithRoom( room: Room<MyPresence, MyStorage, MyUserMeta, never> // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^) { /* ... */}

This is no longer needed. You can simply remove them. TypeScript will still know about your custom age property on user.info.

import type { Room, User } from "@liveblocks/client";
// ✅ Afterfunction isAdult(user: User) { return user.info.age >= 18; // ^^^ Still coming from configured UserMeta custom type}
// ✅ Afterfunction doSomethingWithRoom(room: Room) { /* ... */}

Improvements

Furthermore, the following miscellaneous quality-of-life improvements have been made that are non-breaking changes.

ClientSideSuspense no longer needs a function

Previously, the ClientSideSuspense helper needed a function as its children prop, but it no longer has to.

$npx @liveblocks/codemod@latest simplify-client-side-suspense-children

This will change:

// ❌ Before<ClientSideSuspense fallback={<Loading />}>  {() => <MyApp />}</ClientSideSuspense>
// ✅ After<ClientSideSuspense fallback={<Loading />}>  <MyApp /></ClientSideSuspense>

Improved InboxNotification props types

When passing custom components to the kinds prop of InboxNotification, you could use types like InboxNotificationThreadProps for the props. But this wasn’t always true for all notification kinds, so now you can use types named InboxNotificationThreadKindProps for your components, while InboxNotificationThreadProps describes the props of our own InboxNotification.Thread.

// ❌ Beforefunction MyThreadNotification(props: InboxNotificationThreadProps) {  return <InboxNotification.Thread {...props} />;}
<InboxNotification kinds={{ thread: MyThreadNotification }} />;
// ✅ Afterfunction MyThreadNotification(props: InboxNotificationThreadKindProps) {  //                                                        ^^^^  return <InboxNotification.Thread {...props} />;}
<InboxNotification kinds={{ thread: MyThreadNotification }} />;

New custom type RoomInfo

By using the resolveRoomsInfo callback from createClient or the new LiveblocksProvider, you can attach arbitrary room data to a room, which you can retrieve with the useRoomInfo hook.

Both of these APIs will now respect the type you provide via:

declare global {  interface Liveblocks {    RoomInfo: {      /* your custom type definition here */    };  }}

New custom type ActivitiesData

By providing a custom ActivitiesData type, you can improve how your custom notifications and their activities’ data are typed.

declare global {  interface Liveblocks {    // Custom activities data for custom notification kinds    ActivitiesData: {      // Example, a custom $alert kind      $alert: {        title: string;        message: string;      };    };
// Other kinds // ... }}

That’s it!

Questions? Please ask!

If you have any trouble with these new patterns, run into a bug with one of the codemods, or otherwise need help, please let us know by email or by joining our Discord community! We’re here to help!