How to add live cursors to your product with Next.js and Liveblocks using the public key API.

Learn how to build interactive web apps with Liveblocks.

Almost everyone loves real-time collaborating tools especially developers. Take the Figma real-time indication of viewers' cursor for example. When on a Figma design board, you can see exactly where other viewers are. It’s awesome, right? I bet we all love it. This real-time feature/experience is exactly what Liveblocks provides. With Liveblocks, we don’t have to use WebSockets directly or use the popular socket.io. We can add real-time experience to our product using some of the Liveblocks integrations. Click here to read a more detailed overview of Liveblocks.

In this article, we will learn how to add a live cursor to a Next.js app using Liveblocks public key API. To do this, we will clone a Next.js app (Windbnb - a mini Airbnb clone), integrate it with Liveblocks, and add a live cursor to it.

Get started by cloning the Next.js app from Github. Open your terminal and run the command below.

git clone https://github.com/Origho-precious/windbnb-next.git

You should have git installed on your computer for the above command to work. If you don’t have git installed already, you can do that from here. After downloading and installing git, run the above command to clone the project.

After cloning the repo, navigate into the project’s folder and run the command below to install the project’s dependencies.

yarn install

OR

npm install

Open the project in your favorite code editor. The folder structure should look like this.

📦public
 ┣ 📜favicon.ico
 ┗ 📜vercel.svg
📦src
 ┣ 📂components
 ┃ ┣ 📂Cursor
 ┃ ┃ ┗ 📜Cursor.jsx
 ┃ ┗ 📂Nav
 ┃ ┃ ┣ 📜Nav.jsx
 ┃ ┃ ┗ 📜nav.module.css
 ┣ 📂pages
 ┃ ┣ 📜_app.js
 ┃ ┗ 📜index.js
 ┣ 📂styles
 ┃ ┣ 📜Home.module.css
 ┃ ┗ 📜globals.css
 ┗ 📜data.js

Let’s quickly go over the components and some of the files we have there. In the src folder, we have three folders;

  • components: this folder contains two components
    • Nav, the Nav component handles searching in the app, and
    • Cursor, we will use this to indicate the presence of other users in the app with the help of Liveblocks.
  • pages: since this is a Next.js app, the page handles the route, and there we have a file named index.js. This page renders some Airbnb locations in Finland. And also the Nav for searching.
  • styles: this houses the app’s CSS files.

Integrating Liveblocks with Next.js.

In this section, we will integrate our windbnb app with Liveblocks and add live cursors. To achieve this, we need to signup on liveblocks.io to get a public key with which we can successfully integrate our app with Liveblocks.

Let’s head over to the Liveblocks website by clicking here. On the navbar, you’ll see a “Sign up” link, click on it to signup. After signing up, you will be logged in automatically and routed to your dashboard. Your dashboard should look like this.

Liveblocks dashboard

Check your email for a verification mail from Liveblocks to activate your account. In your dashboard, click on “API Keys” on the sidebar and copy your public key. For security reasons, you might want to create a .env file and save the public key there so it’s not accessible to everyone.

For the integration, we will need two packages provided by Liveblocks;

  • @liveblocks/client: This package lets you create a client to connect to Liveblocks servers.
  • @liveblocks/react: This package provides hooks to make it easier to use the Liveblocks client in a React app.

We will get a better understanding when we use them in the next section. In your terminal, run the command below to install them.

yarn add @liveblocks/client @liveblocks/react

OR

npm install @liveblocks/client @liveblocks/react

N.B: This article assumes you have a basic understanding of React and Nextjs, so it focuses on explaining Liveblocks integration and not React and Nextjs specific syntax.

In your code editor, navigate to src/pages/_app.js. We will add Liveblocks integration in there by updating the content of the file with the code below.

import { createClient } from "@liveblocks/client";
import { LiveblocksProvider } from "@liveblocks/react";
import "../styles/globals.css";

const client = createClient({
  publicApiKey: {YOUR PUBLIC KEY}, 
});

function MyApp({ Component, pageProps }) {
  return (
    <LiveblocksProvider client={client}>
      <Component {...pageProps} />
    </LiveblocksProvider>
  );
}
export default MyApp;

We’ve successfully configured our app to use Liveblocks. We imported;

  • createClient; This function creates a client using a public key. With this client we can communicate with Liveblocks servers.
  • LiveblocksProvider. This is a context provider for the state of the client we created with createClient as in React Context API.

We then passed the client we created with our public key to LiveblocksProvider and wrapped the entire app with it, thereby making data from the server available to be consumed by any component in the app.

Next, we will create a Room that any user viewing the page will be connected to. To achieve this, we need to create a Component to show the presence of users on the page including our presence. Navigate to src/components in here, create a folder called Presence and a file inside it called Presence.jsx. Add the code below to the file.

import { useMyPresence, useOthers } from "@liveblocks/react";
import Cursor from "../Cursor/Cursor";

const COLORS = [
  "#E57373",
  "#9575CD",
  "#4FC3F7",
  "#81C784",
  "#FFF176",
  "#FF8A65",
  "#F06292",
  "#7986CB",
];

const Presence = () => {
  const [{ cursor }, updateMyPresence] = useMyPresence();
  const others = useOthers();

  return (
    <div
      style={{
        position: "fixed",
        height: "100vh",
        left: 0,
        top: 0,
        width: "100vw",
        zIndex: 5,
      }}
      onPointerMove={(event) =>
        updateMyPresence({
          cursor: {
            x: Math.round(event.clientX),
            y: Math.round(event.clientY),
          },
        })
      }
      onPointerLeave={() =>
        updateMyPresence({
          cursor: null,
        })
      }
    >
      <div
        style={{
          textAlign: "left",
          color: "#81C784",
          transform: `translateX(${cursor?.x || 0}px) translateY(${
            cursor?.y || 0
          }px)`,
        }}
      >
        {cursor ? `ME` : ""}
      </div>
      {others.map(({ connectionId, presence }) => {
        if (presence == null || presence.cursor == null) {
          return null;
        }
        return (
          <Cursor
            key={`cursor-${connectionId}`}
            color={COLORS[connectionId % COLORS.length]}
            x={presence.cursor.x}
            y={presence.cursor.y}
          />
        );
      })}
    </div>
  );
};

export default Presence;

Above, We imported useMyPresence, and useOthers from @liveblocks/react.

  • useMyPresence is a hook. It contains the current position of the current user (that is, you viewing the page) and a function to update it.
  • useOthers is also a hook. It returns every other user connected to the room. but unlike useMyPresence, it doesn’t have a setter function.

I know the concept of a room might sound strange. When using Liveblocks, you have to create a room and each room should have a unique Id. This is done using a component provided by Liveblocks; RoomProvider. Only components wrapped by the RoomProvider will be able to access the data from other users in the room.

After that, we created an array of HEX colors, we will dynamically assign these colors to the cursor of other users on that page. Using the data from useMyPresence and useOthers, we rendered some jsx to show the position of the current user and of others on the page.

Now let’s create a room and wrap the page with it by navigating to src/pages/index.js and updating the code there to be the one below.

import { useEffect, useState } from "react";
import Head from "next/head";
import { RoomProvider } from "@liveblocks/react";
import styles from "../styles/Home.module.css";
import SearchNav from "../components/Nav/Nav";
import data from "../data";
import Presence from "../components/Presence/Presence";

const Home = () => {
  const [search, setSearch] = useState("");
  const [results, setResults] = useState(data);
  const [stays, setStays] = useState(data.length);
  useEffect(() => {
    const processSearchResult = () => {
      if (search) {
        const filteredResult = data.filter((item) => {
          return (
            `${item.city}, ${item.country}` === search.location &&
            item.maxGuests > search.guests
          );
        });
        setResults(filteredResult);
        setStays(filteredResult.length);
      }
    };
    processSearchResult();
  }, [search]);

  const renderHouses = () => {
    return results.map((item) => {
      return (
        <div className={styles.gridItem} key={item.title}>
          <img src={item.photo} alt={item.title} />
          <div className={styles.texts}>
            {item.superHost ? (
              <span className={styles.superHost}>SUPERHOST</span>
            ) : null}
            <span>
              {item.type} &nbsp; {item.beds ? `${item.beds} beds` : null}
            </span>
            <span>
              <i className="fas fa-star"></i> {item.rating}
            </span>
          </div>
          <p>{item.title}</p>
        </div>
      );
    });
  };

  return (
    <div className="App">
      <Head>
        <meta name="theme-color" content="#EF7B7B" />
        <meta
          name="description"
          content="Windbnb: Airbnb clone for stays in Finland"
        />
        <link rel="icon" href="/favicon.ico" />
        <link
          rel="stylesheet"
          href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta2/css/all.min.css"
          integrity="sha512-YWzhKL2whUzgiheMoBFwW8CKV4qpHQAEuvilg9FAn5VJUDwKZZxkJNuGM4XkWuk94WCrrwslk8yWNGmY1EduTA=="
          crossOrigin="anonymous"
          referrerpolicy="no-referrer"
        />
        <title>Windbnb - Airbnb Clone</title>
      </Head>
      <RoomProvider
        id="windbnb"
        defaultPresence={() => ({
          cursor: null,
        })}
      >
        <Presence />
        <SearchNav getSearch={setSearch} />
        <main className={styles.Home}>
          <header>
            <h2>Stays in Finland</h2>
            <p>{stays ? `${stays} stay(s)` : "12+ stays"}</p>
          </header>
          <div className={styles.grid}>{renderHouses()}</div>
        </main>
      </RoomProvider>
    </div>
  );
};

export default Home;

We have now completely added live cursor to our Next.js app using Liveblocks. You can go ahead and test the app by starting the dev server with the command below.

yarn dev

OR

npm run dev

Go to your browser and visit localhost:3000. If you followed along very well, you should see a page like the one below.

If you see any errors in your console or browser, please re-read the article from the beginning and compare your code with the final version (link in the next section).

Resources for more learning.