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 componentsNav
, the Nav component handles searching in the app, andCursor
, 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.
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 unlikeuseMyPresence
, 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} {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).