Creating a useMetadata React hook with SWR and Liveblocks

Liveblocks is one of the easiest and most powerful options for real-time apps, but you need to implement a few things yourself to make the metadata update in real-time.

This post will show you how to do that with SWR, a React hook for data fetching by Vercel and Shu. It's the simplest way I've found to make editable metadata feel live throughout your app.

This is particularly handy because in order to query and filter rooms, e.g. by creator, you need your criteria to be stored in metadata.

Setting up an API route

You'll need to make an API route to update metadata on the backend. This is necessary for the useSWR hook from SWR to work properly. For example, api/version/liveblocks/metadata/route.ts might look like this:

import { NextRequest, NextResponse } from 'next/server';
import axios from 'axios';

const requestConfig = {
  headers: {
    Authorization: `Bearer ${process.env.LIVEBLOCKS_SECRET_KEY}`
  }
}

interface Props {
  params: {
    room: string
  }
}

export async function GET(req: NextRequest, { params }: Props) {
  const res = await axios.get(`https://api.liveblocks.io/v2/rooms/${params.room}`, requestConfig)
  return NextResponse.json(res.data.metadata)
}

export async function POST(req: NextRequest, { params }: Props) {

  /*
   * Not shown: make sure the user has permission to make an edit.
   */

  const json = await req.json()
  const allowedFields = ['title', 'emoji'] // for example
  const metadata: { [key: string]: string } = {}

  allowedFields.forEach(field => {
    if (json[field]) { metadata[field] = json[field] }
  })

  await axios.post(`https://api.liveblocks.io/v2/rooms/${params.room}`, {
    metadata
  }, requestConfig)

  return NextResponse.json({
    success: true
  })
}

Now you're ready to add your React hook for useMetadata. This is similar to what Chris Nicholas from Liveblocks has posted here and what I've posted here, but abstracted into a React Hook for use throughout your components. It also gives you flexibility, for example if you want to update locally with each keystroke but remotely only when a change is complete.

In e.g. hooks/metadata.ts, you would write:


'use client'

import axios from 'axios';
import useSWR from 'swr'
import useSWRMutation from 'swr/mutation'
import { useBroadcastEvent, useEventListener } from '@/liveblocks.config'

async function postUpdate(url: string, { arg }: { arg: { [key: string]: string }}) {
  await axios.post(url, arg);
}

export const useMetadata = (roomId: string) => {
  const metadataPath = `/api/v1/s/${roomId}/metadata`
  const fetchMeta = (url: string) => axios.get(url).then(res => {
    return res.data
  })
  const metadata = useSWR(metadataPath, fetchMeta)
  const setMetadata = useSWRMutation(metadataPath, postUpdate)
  const broadcastMeta = useBroadcastEvent();

  function refreshMetadata() {
    metadata.mutate();
    (broadcastMeta as (event: { type: string }) => void)({ type: 'REVALIDATE' });
  }

  function updateMetadata(updates: { [key: string]: string }, callback?: () => void) {
    setMetadata.trigger(updates, {
      optimisticData: (current: any) => ({
        ...current,
        ...updates
      })
    }).then(() => {
      if (callback) { callback() }
      refreshMetadata()
    })
  }

  useEventListener(({ event }: { event: { type: string }}) => {
    if (event.type === 'REVALIDATE') {
      metadata.mutate()
    }
  })

  return {
    metadata: metadata.data,
    updateMetadata: updateMetadata,
    refreshMetadata: refreshMetadata,
  }
};

This exports three objections that you can now use in your components: metadata, updateMetadata, and refreshMetadata. (The last one isn't used but comes in handy if you need to manually apply and broadcast an update.) Now in one of your components you could implement the following to edit a title.

import { useMetadata } from '@/hooks/metadata';

export function TitleChanger({ roomId }: { roomId: string }) {
  const [title, setTitle] = useState('')
  const { metadata, updateMetadata } = useMetadata(roomId)

  return (
    <input placeholder="Title" value={title} 
    onKeyDown={(e) => {
      if (e.key === 'Enter') {
        e.currentTarget.blur()
      }
    }}
    onChange={(e) => {
      setTitle(e.currentTarget.value)
    }}
    onBlur={(e) => {
      updateMetadata({ title: e.currentTarget.value })
    }} />
  )
}

Notice how little logic is in the component itself. You can now use this hook throughout your app to make metadata updates feel simple and real-time.