At Liveblocks, we strive to craft ergonomic and easy-to-use APIs that allow
developers to build performant multiplayer experiences. With 0.18, we’re making
some important changes in that direction:

- [React hooks that return immutable data](#react-hooks-immutable-data)
- [React selector APIs](#react-selector-apis)
- [React Suspense support](#react-suspense-support)
- [Improved core performance](#improved-core-performance)
- [Specialized React hooks for optimized use cases](#react-specialized-hooks)

The new APIs we’re introducing here solve many subtle and not-so-subtle pain
points. We heard your feedback, and think we have shipped something awesome that
you’ll love.

## New React hooks that return immutable data [#react-hooks-immutable-data]

With 0.18, we’re introducing two main new hooks: `useStorage` and `useMutation`,
which should make reading and writing to Storage much more natural in a React
world. The biggest conceptual shift is that the `useStorage` hook lets you
consume data using normal JavaScript data structures (objects, arrays, maps)
that are immutable.

This means you’ll no longer have to deal with `LiveObject`, `LiveList`, or
`LiveMap` when reading data—only when mutating!

While the `useObject`, `useList`, and `useMap` hooks will still work in 0.18, we
recommend migrating to `useStorage` and `useMutation`. We’ve written
[a step-by-step guide](/docs/guides/upgrading/0.18) to help you migrate. If
you’re stuck, please reach out to us on
[GitHub](https://github.com/liveblocks/liveblocks), and we’ll lend you a hand!

### `useStorage`

This improves the developer experience significantly, enabling you to turn code
that looks like this…

```tsx
// ❌ Before
function Component() {
  const scientist = useObject("scientist");
  if (scientist == null) {
    return null;
  }

  const pets = scientist.get("pets").toArray();
  // ["🐶", "🐈"]
}
```

into code that looks like this…

```tsx
// ✅ Now
function Component() {
  const pets = useStorage((root) => root.scientist.pets);
  // ["🐶", "🐈"]
}
```

Much better, right?

But that’s not all. Rerendering your components when nested data—like our
scientist’s pets list—changes was a true head breaker before. This required
extra helper components, manual subscriptions, and manual conversion to “normal”
JavaScript arrays.

```tsx
// ❌ Before
function Component() {
  const scientist = useObject("scientist");
  if (scientist == null) {
    return null;
  }

  return <Pets livePets={scientist.get("pets")} />;
}

function Pets({ livePets }) {
  const room = useRoom();
  const [pets, setPets] = useState(livePets.toArray());

  useEffect(() => {
    return room.subscribe(livePets, () => {
      setPets(livePets.toArray());
    });
  }, [room, livePets]);
}
```

Now, rerendering your component when data changes is automatic, even for deeply
nested data.

```tsx
// ✅ Now
function Component() {
  const pets = useStorage((root) => root.scientist.pets);
  // ["🐶", "🐈"]
}
```

`useStorage` is also helpful when you need to derive a computed value from
different objects. Previously, it took some manual setup to ensure the component
would automatically rerender when either of those values changed.

```tsx
// ❌ Before
function Component() {
  const objA = useObject("a");
  const objB = useObject("b");

  const room = useRoom();
  const [sum, setSum] = useState();
  //     ^^^ We’re trying to compute the result of a.x + b.x in here

  useEffect(() => {
    function onChange() {
      setSum(objA.get("x") + objB.get("x"));
    }

    const unsubA = room.subscribe(objA, onChange);
    const unsubB = room.subscribe(objB, onChange);

    return () => {
      unsubA();
      unsubB();
    };
  }, [room, objA, objB]);
}
```

The code can now be simplified as follow to rerender automatically any time
`a.x` or `b.x` changes, but not more often.

```js
// ✅ Now
function Component() {
  const sum = useStorage((root) => root.a.x + root.b.x);
}
```

Now, this is fully automatic. Or should we say, automagic? `useStorage` feels
right at home in React now: say bye to those redundant `useEffect` with
`room.subscribe` hooks!

### `useMutation`

Now that reading from the Liveblocks storage state is all read-only and no
longer returns mutable data structures, how do we mutate things? The answer is
the new `useMutation` hook.

It’s almost like a normal React `useCallback`, but it provides you access to the
mutable Storage root through the first callback argument. For instance, here is
what a fill action could look like in a design tool. Simply pass the id and the
color of the shape you want to fill.

```tsx
const fill = useMutation(
  // Note the second and third arguments
  ({ storage, setMyPresence }, shapeId: string, color: string) => {
    storage.get("shapes").get(shapeId).set("fill", color);
    setMyPresence({ lastUsedColor: color });
  },
  []
);

return <button onClick={() => fill("shape1", "red")} />;
```

Notice how we’re able to also set the last used color for the current user.
Handy, right? Especially when you start using this for more advanced scenarios
like deleting all the shapes that are currently selected.

```tsx
const deleteSelectedShapes = useMutation(
  // You can use the current "self" or "others" state in the mutation
  ({ storage, self, others, setMyPresence }) => {
    // Delete the selected shapes
    const shapes = storage.get("shapes");
    for (const shapeId of self.presence.selectedShapeIds) {
      shapes.delete(shapeId);
    }

    // Clear the current selection
    setMyPresence({ selectedShapeIds: [] });
  },
  []
);

return <button onClick={deleteSelectedShapes} />;
```

With `useMutation`, you can now write custom mutation logic for your application
where both presence and storage work seamlessly together—enabling you to build
world-class multiplayer experiences, no matter how complex the experience you’re
trying to build is.

What’s also nice about it is that it allows you the choice of how to organize
your mutation code. You can colocate it directly in your components, or
centralize them as common actions, making it easier for larger teams to interact
with the state at scale.

To read more about how mutations work exactly, please refer to the
[`useMutation`](/docs/api-reference/liveblocks-react#useMutation) API reference.

## React selector APIs

As you may have seen in the section above, `useStorage` takes a selector
function. With 0.18, we’re also bringing these selectors to the existing
`useSelf` and `useOthers` hooks. The selector APIs receive immutable data,
return arbitrary values, and automatically subscribe to updates.

The received input to all selector functions is a read-only and immutable
top-level context value that differs for each hook:

- `useStorage((root) => ...)` receives the Storage root
- `useSelf((me) => ...)` receives the current user
- `useOthers((others) => ...)` receives a list of other users in the room
- `useOthersMapped((other) => ...)` receives each individual other user in the
  room
- `useOther(connectionId, (other) => ...)` receives a specific user in the room

```tsx
const animals = useStorage((root) => root.animals);
// ["🦁", "🦊", "🐵"]

const ada = useStorage((root) => root.mathematician);
// { firstName: "Ada", lastName: "Lovelace" }

const fullname = useStorage(
  (root) => `${root.mathematician.firstName} ${root.mathematician.lastName}`
);
// "Ada Lovelace"

const fruits = useStorage((root) => [...root.fruitsByName.values()], shallow);
// ["🍎", "🍌", "🍒"]
```

For full details, you can read more in the
[how selectors work](/docs/api-reference/liveblocks-react#selectors) section of
our documentation.

## React Suspense support

The benefit of using
[Suspense](https://reactjs.org/docs/react-api.html#suspense) with Liveblocks is
that the hooks will no longer return `null` when Liveblocks is still loading.
Instead, you can let your Suspense boundary handle the still-loading case
centrally by showing the fallback state.

This improves the developer experience significantly, enabling you to turn code
that looks like this…

```tsx
function MyComponent() {
  const cursor = useSelf((me) => me.presence.cursor);

  if (cursor === null) {
    return null;
  }

  const { x, y } = cursor;
  return <Cursor x={x} y={y} />;
}
```

into code that looks like this…

```tsx
function MyComponent() {
  const { x, y } = useSelf((me) => me.presence.cursor);
  return <Cursor x={x} y={y} />;
}
```

Much better, right?

If you want to learn more about using Suspense with React, please make sure to
[read the guide](/docs/api-reference/liveblocks-react#suspense) in the
documentation.

## Improved core performance

For this release, we’ve had to refactor several parts of our core. Not only did
this make the `@liveblocks/react` package size go down from 12.6kB to 8.2kB
minified, but it also made the core faster, more scalable, and future-proof.

We’ve also made several changes to the current functionalities in Liveblocks
core. Setting the initial presence upon entering a room is now required, for
example, either by calling `client.enter()` or using `RoomProvider`. This means
that when a user enters the room, their presence is instantly recognized. In the
past, there would be a brief moment when a user’s presence data wasn’t known yet
and it would be unknown, causing a UI flash in some situations. But this is no
longer the case — either the user is fully known, or they will not show up in
the room until then.

## Specialized React hooks for optimized use cases [#react-specialized-hooks]

Lastly, in addition to the `useStorage` and `useMutation` hooks above, we added
a few more specialized pragmatic hooks to assist with building for two common
use cases where rendering performance matters:

- `useOthersMapped` to select subsets of others
- `useOthersConnectionIds` and `useOther` to build highly optimized UIs

These can be particularly useful for rendering live cursors in highly
interactive applications where multiple people are simultaneously editing the
same document.

If you’re interested in learning more, please read the API reference for
[`useOthersMapped`](/docs/api-reference/liveblocks-react#useOthersMapped),
[`useOthersConnectionIds`](/docs/api-reference/liveblocks-react#useOthersConnectionIds),
and [`useOther`](/docs/api-reference/liveblocks-react#useOther).

## Contributors

Huge thanks to everyone who contributed! Keep checking out the
[changelog](https://github.com/liveblocks/liveblocks/blob/main/CHANGELOG.md#v0180)
for the full release notes – and see you next time!