From 1e2a1debd016caa175c7ce11beef7c82f342901a Mon Sep 17 00:00:00 2001 From: Fiona Corden Date: Thu, 16 Apr 2026 17:44:10 +0100 Subject: [PATCH] examples/with-ably: modernize to App Router + Ably v2 Replaces Pages Router with App Router, upgrades to ably v2 (hooks bundled into ably/react), and creates the Realtime client inside useEffect so no connection is attempted during SSR. Tracks ably/ably-nextjs-fundamentals-kit#11. --- examples/with-ably/README.md | 175 +++++++++++------- .../with-ably/app/ably-client-provider.tsx | 64 +++++++ .../app/api/createTokenRequest/route.ts | 17 ++ .../with-ably/app/api/send-message/route.ts | 23 +++ examples/with-ably/app/home-client.tsx | 103 +++++++++++ examples/with-ably/app/layout.tsx | 23 +++ examples/with-ably/app/page.tsx | 5 + examples/with-ably/package.json | 15 +- examples/with-ably/pages/_app.tsx | 18 -- .../with-ably/pages/api/createTokenRequest.ts | 13 -- examples/with-ably/pages/api/send-message.ts | 19 -- examples/with-ably/pages/index.tsx | 114 ------------ examples/with-ably/styles/Home.module.css | 34 ---- examples/with-ably/styles/globals.css | 35 ++++ examples/with-ably/tsconfig.json | 23 ++- 15 files changed, 397 insertions(+), 284 deletions(-) create mode 100644 examples/with-ably/app/ably-client-provider.tsx create mode 100644 examples/with-ably/app/api/createTokenRequest/route.ts create mode 100644 examples/with-ably/app/api/send-message/route.ts create mode 100644 examples/with-ably/app/home-client.tsx create mode 100644 examples/with-ably/app/layout.tsx create mode 100644 examples/with-ably/app/page.tsx delete mode 100644 examples/with-ably/pages/_app.tsx delete mode 100644 examples/with-ably/pages/api/createTokenRequest.ts delete mode 100644 examples/with-ably/pages/api/send-message.ts delete mode 100644 examples/with-ably/pages/index.tsx delete mode 100644 examples/with-ably/styles/Home.module.css diff --git a/examples/with-ably/README.md b/examples/with-ably/README.md index 0332525809b4..db06d6d72fa7 100644 --- a/examples/with-ably/README.md +++ b/examples/with-ably/README.md @@ -1,10 +1,10 @@ -# Realtime Edge Messaging with [Ably](https://ably.com/) +# Realtime messaging with [Ably](https://ably.com/) **Demo:** [https://next-and-ably.vercel.app/](https://next-and-ably.vercel.app/) Add realtime data and interactive multi-user experiences to your Next.js apps with [Ably](https://ably.com/), without the infrastructure overhead. -Use Ably in your Next.js application using idiomatic, easy to use hooks. +Use Ably in your Next.js App Router application with the `ably/react` hooks. Using this demo you can: @@ -12,13 +12,13 @@ Using this demo you can: - Get notifications of [user presence](https://ably.com/docs/realtime/presence) on channels - Send [presence updates](https://ably.com/docs/api/realtime-sdk/presence#update) when a new client joins or leaves the demo -This demo is uses the [Ably React Hooks package](https://www.npmjs.com/package/@ably-labs/react-hooks), a simplified syntax for interacting with Ably, which manages the lifecycle of the Ably SDK instances for you taking care to subscribe and unsubscribe to channels and events when your components re-render). +This demo uses the Ably React hooks that ship with the [`ably`](https://www.npmjs.com/package/ably) package, which manages the lifecycle of the Ably SDK instances for you, subscribing and unsubscribing to channels and events as your components mount and unmount. ## Deploy your own -**You will need an Ably API key to run this demo, [see below for details](#ably-setup)** +**You will need an Ably API key to run this demo. [See below for details](#ably-setup).** -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-ably&project-name=with-ably&repository-name=with-ably) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-ably&project-name=with-ably&repository-name=with-ably&env=ABLY_API_KEY&envDescription=Ably%20API%20key%20from%20https%3A%2F%2Fably.com%2F) ## How to use @@ -38,101 +38,132 @@ pnpm create next-app --example with-ably with-ably-app Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). -**When deployed, ensure that you set your environment variables (the Ably API key and the deployed Vercel API root) in your Vercel settings** +**When deployed, ensure that you set your `ABLY_API_KEY` environment variable in your Vercel project settings.** ## Notes -### Ably Setup +### Ably setup In order to send and receive messages you will need an Ably API key. If you are not already signed up, you can [sign up now for a free Ably account](https://www.ably.com/signup). Once you have an Ably account: 1. Log into your app dashboard. -2. Under **“Your apps”**, click on **“Manage app”** for any app you wish to use for this tutorial, or create a new one with the “Create New App” button. -3. Click on the **“API Keys”** tab. -4. Copy the secret **“API Key”** value from your Root key. -5. Create a .env file in the root of the demo repository -6. Paste the API key into your new env file, along with a env variable for your localhost: +2. Under **"Your apps"**, click on **"Manage app"** for any app you wish to use for this tutorial, or create a new one with the "Create New App" button. +3. Click on the **"API Keys"** tab. +4. Copy the secret **"API Key"** value from your Root key. +5. Create a `.env.local` file in the root of the project. +6. Paste the API key into your new env file: ```bash ABLY_API_KEY=your-ably-api-key:goes-here -API_ROOT=http://localhost:3000 ``` -### How it Works/Using Ably - -#### Configuration - -[pages/\_app.js](pages/_app.js) is where the Ably SDK is configured: - -```js -import { configureAbly } from "@ably-labs/react-hooks"; - -const prefix = process.env.API_ROOT || ""; -const clientId = - Math.random().toString(36).substring(2, 15) + - Math.random().toString(36).substring(2, 15); - -configureAbly({ - authUrl: `${prefix}/api/createTokenRequest?clientId=${clientId}`, - clientId: clientId, -}); - -function MyApp({ Component, pageProps }) { - return ; +### How it works + +#### Client provider + +[`app/ably-client-provider.tsx`](app/ably-client-provider.tsx) is a Client Component that creates the Ably Realtime client inside a `useEffect`, so no connection is attempted during SSR. It wraps the app in `AblyProvider` (and `ChannelProvider`) from `ably/react`, making the hooks available to child Client Components: + +```tsx +"use client"; + +import { useEffect, useState } from "react"; +import * as Ably from "ably"; +import { AblyProvider, ChannelProvider } from "ably/react"; + +export default function AblyClientProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [clientId] = useState( + () => + Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15), + ); + const [client, setClient] = useState(null); + + useEffect(() => { + const ably = new Ably.Realtime({ + authUrl: `/api/createTokenRequest?clientId=${clientId}`, + clientId, + }); + setClient(ably); + return () => { + ably.close(); + }; + }, [clientId]); + + if (!client) return <>{children}; + + return ( + + + + {children} + + + + ); } - -export default MyApp; ``` -`configureAbly` matches the method signature of the Ably SDK - and requires either a string or a [AblyClientOptions](https://ably.com/docs/api/realtime-sdk#client-options) object. You can use this configuration object to setup your [tokenAuthentication](https://ably.com/docs/core-features/authentication#token-authentication). If you want to use the usePresence function, you'll need to explicitly provide a `clientId`. - -You can do this anywhere in your code before the rest of the library is used. +The client is authenticated via a [token request](https://ably.com/docs/core-features/authentication#token-authentication) served by a Route Handler at [`app/api/createTokenRequest/route.ts`](app/api/createTokenRequest/route.ts), so your `ABLY_API_KEY` is never exposed to the browser. -#### useChannel (Publishing and Subscribing to Messages) +#### useChannel (publishing and subscribing to messages) The `useChannel` hook lets you subscribe to a channel and receive messages from it: -```js +```tsx +"use client"; + import { useState } from "react"; -import { useChannel } from "@ably-labs/react-hooks"; +import { useChannel } from "ably/react"; +import type * as Ably from "ably"; -export default function Home() { - const [channel] = useChannel("your-channel", async (message) => { +export default function ChatArea() { + const [messages, setMessages] = useState([]); + + const { channel } = useChannel("some-channel-name", (message) => { console.log("Received Ably message", message); + setMessages((prev) => [...prev, message]); }); -} -``` - -Every time a message is sent to `your-channel` it will be logged to the console. You can do whatever you need to with those messages. - -##### Publishing a message -The `channel` instance returned by `useChannel` can be used to send messages to the channel. It is a regular Ably JavaScript SDK `channel` instance. + // publish a message + const send = () => channel.publish("test-message", { text: "hello" }); -```javascript -channel.publish("test-message", { text: "message text" }); + return ; +} ``` -#### usePresence - -The `usePresence` hook lets you subscribe to presence events on a channel - this will allow you to get notified when a user joins or leaves the channel. The presence data is automatically updated and your component re-rendered when presence changes: - -```js -import { useState } from "react"; -import { usePresence } from "@ably-labs/react-hooks"; - -export default function Home() { - const [presenceData, updateStatus] = usePresence("your-channel-name"); - - const presentClients = presenceData.map((msg, index) => ( -
  • - {msg.clientId}: {msg.data} -
  • - )); - - return
      {presentClients}
    ; +#### usePresence and usePresenceListener + +`usePresence` enters presence and lets you update your presence data. `usePresenceListener` subscribes to presence changes on a channel: + +```tsx +"use client"; + +import { usePresence, usePresenceListener } from "ably/react"; + +export default function Presence() { + const { updateStatus } = usePresence("your-channel-name"); + const { presenceData } = usePresenceListener("your-channel-name"); + + return ( + <> + +
      + {presenceData.map((msg, i) => ( +
    • + {msg.clientId}: {String(msg.data ?? "")} +
    • + ))} +
    + + ); } ``` -You can read more about the hooks available with the Ably Hooks package on the [@ably-labs/ably-hooks documentation on npm](https://www.npmjs.com/package/@ably-labs/react-hooks). +You can read more about the hooks in the [Ably React docs](https://ably.com/docs/getting-started/react). diff --git a/examples/with-ably/app/ably-client-provider.tsx b/examples/with-ably/app/ably-client-provider.tsx new file mode 100644 index 000000000000..5c7e4bacfea8 --- /dev/null +++ b/examples/with-ably/app/ably-client-provider.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { + createContext, + useContext, + useEffect, + useState, + type ReactNode, +} from "react"; +import * as Ably from "ably"; +import { AblyProvider, ChannelProvider } from "ably/react"; + +const AblyReadyContext = createContext(false); + +export function useAblyReady() { + return useContext(AblyReadyContext); +} + +function randomClientId() { + return ( + Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15) + ); +} + +export default function AblyClientProvider({ + children, +}: { + children: ReactNode; +}) { + const [clientId] = useState(() => randomClientId()); + const [client, setClient] = useState(null); + + useEffect(() => { + const ably = new Ably.Realtime({ + authUrl: `/api/createTokenRequest?clientId=${clientId}`, + clientId, + }); + setClient(ably); + return () => { + ably.close(); + }; + }, [clientId]); + + if (!client) { + return ( + + {children} + + ); + } + + return ( + + + + + {children} + + + + + ); +} diff --git a/examples/with-ably/app/api/createTokenRequest/route.ts b/examples/with-ably/app/api/createTokenRequest/route.ts new file mode 100644 index 000000000000..8c1c6311b94b --- /dev/null +++ b/examples/with-ably/app/api/createTokenRequest/route.ts @@ -0,0 +1,17 @@ +import * as Ably from "ably"; +import { type NextRequest, NextResponse } from "next/server"; + +export async function GET(request: NextRequest) { + if (!process.env.ABLY_API_KEY) { + return NextResponse.json( + { error: "Missing ABLY_API_KEY environment variable" }, + { status: 500 }, + ); + } + + const client = new Ably.Rest(process.env.ABLY_API_KEY); + const clientId = + request.nextUrl.searchParams.get("clientId") ?? "NO_CLIENT_ID"; + const tokenRequestData = await client.auth.createTokenRequest({ clientId }); + return NextResponse.json(tokenRequestData); +} diff --git a/examples/with-ably/app/api/send-message/route.ts b/examples/with-ably/app/api/send-message/route.ts new file mode 100644 index 000000000000..5ace37047aae --- /dev/null +++ b/examples/with-ably/app/api/send-message/route.ts @@ -0,0 +1,23 @@ +import * as Ably from "ably"; +import { type NextRequest, NextResponse } from "next/server"; +import type { ProxyMessage, TextMessage } from "../../../types"; + +export async function POST(request: NextRequest) { + if (!process.env.ABLY_API_KEY) { + return NextResponse.json( + { error: "Missing ABLY_API_KEY environment variable" }, + { status: 500 }, + ); + } + + const body = (await request.json()) as ProxyMessage; + const client = new Ably.Rest(process.env.ABLY_API_KEY); + const channel = client.channels.get("some-channel-name"); + + const message: TextMessage = { + text: `Server sent a message on behalf of ${body.sender}`, + }; + await channel.publish("test-message", message); + + return NextResponse.json({ ok: true }); +} diff --git a/examples/with-ably/app/home-client.tsx b/examples/with-ably/app/home-client.tsx new file mode 100644 index 000000000000..0e99b0363054 --- /dev/null +++ b/examples/with-ably/app/home-client.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { useState } from "react"; +import Image from "next/image"; +import * as Ably from "ably"; +import { useChannel, usePresence, usePresenceListener } from "ably/react"; +import type { ProxyMessage, TextMessage } from "../types"; +import { useAblyReady } from "./ably-client-provider"; + +export default function HomeClient() { + const ablyReady = useAblyReady(); + + return ( +
    +

    Realtime Edge Messaging with Next and Ably

    +

    + Use the buttons below to send and receive messages or to update your + status. +

    + {ablyReady ? :

    Connecting to Ably...

    } + +
    + ); +} + +function ChatArea() { + const [messages, setMessages] = useState([]); + + const { channel, ably } = useChannel( + "some-channel-name", + (message: Ably.Message) => { + console.log("Received Ably message", message); + setMessages((prev) => [...prev, message.data as TextMessage]); + }, + ); + + const { updateStatus } = usePresence("your-channel-name"); + const { presenceData } = usePresenceListener("your-channel-name"); + + const messageList = messages.map((message, index) => ( +
  • {message.text}
  • + )); + + const presentClients = presenceData.map((msg, index) => ( +
  • + {msg.clientId}: {String(msg.data ?? "")} +
  • + )); + + return ( +
    +

    Present Clients

    + +
      {presentClients}
    + +

    Ably Message Data

    + + +
      {messageList}
    +
    + ); +} diff --git a/examples/with-ably/app/layout.tsx b/examples/with-ably/app/layout.tsx new file mode 100644 index 000000000000..f3d39c6d6e11 --- /dev/null +++ b/examples/with-ably/app/layout.tsx @@ -0,0 +1,23 @@ +import "../styles/globals.css"; +import type { Metadata } from "next"; +import AblyClientProvider from "./ably-client-provider"; + +export const metadata: Metadata = { + title: "Realtime messaging with Next.js and Ably", + description: + "Next.js App Router example using Ably for pub/sub messaging and presence.", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/examples/with-ably/app/page.tsx b/examples/with-ably/app/page.tsx new file mode 100644 index 000000000000..f00e148769ca --- /dev/null +++ b/examples/with-ably/app/page.tsx @@ -0,0 +1,5 @@ +import HomeClient from "./home-client"; + +export default function Home() { + return ; +} diff --git a/examples/with-ably/package.json b/examples/with-ably/package.json index 80583628c108..4c4d1b67908a 100644 --- a/examples/with-ably/package.json +++ b/examples/with-ably/package.json @@ -6,16 +6,15 @@ "start": "next start" }, "dependencies": { - "@ably-labs/react-hooks": "^2.0.4", - "ably": "^1.2.22", + "ably": "^2.21.0", "next": "latest", - "react": "18.1.0", - "react-dom": "18.1.0" + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "devDependencies": { - "@types/node": "^18.0.0", - "@types/react": "^18.0.12", - "eslint-config-next": "12.1.6", - "typescript": "^4.7.3" + "@types/node": "^20.11.0", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "typescript": "^5.5.3" } } diff --git a/examples/with-ably/pages/_app.tsx b/examples/with-ably/pages/_app.tsx deleted file mode 100644 index 2d20661e9a6a..000000000000 --- a/examples/with-ably/pages/_app.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import "../styles/globals.css"; -import { configureAbly } from "@ably-labs/react-hooks"; - -const prefix = process.env.API_ROOT || ""; -const clientId = - Math.random().toString(36).substring(2, 15) + - Math.random().toString(36).substring(2, 15); - -configureAbly({ - authUrl: `${prefix}/api/createTokenRequest?clientId=${clientId}`, - clientId: clientId, -}); - -function MyApp({ Component, pageProps }) { - return ; -} - -export default MyApp; diff --git a/examples/with-ably/pages/api/createTokenRequest.ts b/examples/with-ably/pages/api/createTokenRequest.ts deleted file mode 100644 index b0778a766bfa..000000000000 --- a/examples/with-ably/pages/api/createTokenRequest.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Ably from "ably/promises"; -import type { NextApiRequest, NextApiResponse } from "next"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse, -) { - const client = new Ably.Realtime(process.env.ABLY_API_KEY); - const tokenRequestData = await client.auth.createTokenRequest({ - clientId: req.query.clientId as string, - }); - res.status(200).json(tokenRequestData); -} diff --git a/examples/with-ably/pages/api/send-message.ts b/examples/with-ably/pages/api/send-message.ts deleted file mode 100644 index 34833c0afe7d..000000000000 --- a/examples/with-ably/pages/api/send-message.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Ably from "ably/promises"; -import type { NextApiRequest, NextApiResponse } from "next/types"; -import type { TextMessage } from "../../types"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse, -) { - const client = new Ably.Realtime(process.env.ABLY_API_KEY); - - const channel = client.channels.get("some-channel-name"); - - const message: TextMessage = { - text: `Server sent a message on behalf of ${req.body.sender}`, - }; - channel.publish("test-message", message); - - res.status(200); -} diff --git a/examples/with-ably/pages/index.tsx b/examples/with-ably/pages/index.tsx deleted file mode 100644 index fbbc89454b5a..000000000000 --- a/examples/with-ably/pages/index.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { useState } from "react"; -import { useChannel, usePresence } from "@ably-labs/react-hooks"; -import type { Types } from "ably"; -import type { ProxyMessage, TextMessage } from "../types"; - -import Head from "next/head"; -import Image from "next/image"; -import styles from "../styles/Home.module.css"; - -export default function Home() { - const [messages, setMessages] = useState([]); - - const [channel, ably] = useChannel( - "some-channel-name", - async (message: Types.Message) => { - console.log("Received Ably message", message); - setMessages((messages) => [...messages, message.data]); - }, - ); - - const [presenceData, updateStatus] = usePresence("your-channel-name"); - - const messageList = messages.map((message, index) => { - return
  • {message.text}
  • ; - }); - - const presentClients = presenceData.map((msg, index) => ( -
  • - {msg.clientId}: {msg.data} -
  • - )); - - return ( -
    - - Create Next App - - - - -

    Realtime Edge Messaging with Next and Ably

    -

    - Use the buttons below to send and receive messages or to update your - status. -

    - -
    -

    Present Clients

    - -
      {presentClients}
    - -

    Ably Message Data

    - - -
      {messageList}
    -
    - - -
    - ); -} diff --git a/examples/with-ably/styles/Home.module.css b/examples/with-ably/styles/Home.module.css deleted file mode 100644 index 226af808fe07..000000000000 --- a/examples/with-ably/styles/Home.module.css +++ /dev/null @@ -1,34 +0,0 @@ -.container { - min-height: 100vh; - display: flex; - flex-direction: column; - padding: 1rem; -} - -.main { - flex-grow: 1; -} - -.footer { - display: flex; - justify-content: center; - flex-grow: 0; - column-gap: 1em; - align-items: flex-start; - padding: 2em 0 0; - border-top: 1px solid #eaeaea; - line-height: 32px; -} - -.footer a { - display: flex; - justify-content: center; - align-items: center; - flex-grow: 0; -} - -.logo { - width: 100px; - height: auto; - margin-right: 1rem; -} diff --git a/examples/with-ably/styles/globals.css b/examples/with-ably/styles/globals.css index d7a4e566bfbd..cce49736e60b 100644 --- a/examples/with-ably/styles/globals.css +++ b/examples/with-ably/styles/globals.css @@ -54,3 +54,38 @@ h1 { p { width: 100%; } + +.container { + min-height: 100vh; + display: flex; + flex-direction: column; + padding: 1rem; +} + +.main { + flex-grow: 1; +} + +.footer { + display: flex; + justify-content: center; + flex-grow: 0; + column-gap: 1em; + align-items: flex-start; + padding: 2em 0 0; + border-top: 1px solid #eaeaea; + line-height: 32px; +} + +.footer a { + display: flex; + justify-content: center; + align-items: center; + flex-grow: 0; +} + +.logo { + width: 100px; + height: auto; + margin-right: 1rem; +} diff --git a/examples/with-ably/tsconfig.json b/examples/with-ably/tsconfig.json index 87059fc36328..d20b9a816d28 100644 --- a/examples/with-ably/tsconfig.json +++ b/examples/with-ably/tsconfig.json @@ -1,20 +1,31 @@ { "compilerOptions": { - "target": "es5", + "target": "es2017", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, - "strict": false, + "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, - "incremental": true, "esModuleInterop": true, "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx", - "moduleResolution": "node", - "resolveJsonModule": true + "incremental": true, + "plugins": [ + { + "name": "next" + } + ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], "exclude": ["node_modules"] }