From 2378ce018b6ba2cf0920bf6ffd27dbfc38930afb Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Fri, 17 May 2024 09:35:40 +0100 Subject: [PATCH 1/5] npm install --save ably@2 --- package-lock.json | 16 +++++++++++----- package.json | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index befbd42..d4f5f29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "ably-nextjs-vercel-demo", "version": "0.1.0", "dependencies": { - "ably": "^1.2.44", + "ably": "^2.0.4", "next": "^13.4.19", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -528,16 +528,17 @@ "dev": true }, "node_modules/ably": { - "version": "1.2.50", - "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.50.tgz", - "integrity": "sha512-9uC5lE7wFBR7nOJdltArHU1UDaBu6CTMbMuie+brUuH884fM1mQuFiMzZetxVacygyUG38dp5MFOyOPF9h0RsQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/ably/-/ably-2.0.4.tgz", + "integrity": "sha512-mA2Zsv7u29jZSCyz9YOXoxWq8GIktcvA57GTIdMP0Xv3byV41bBoAZUgXMl7VjD5cioA6xenUbqX17d9zcimSw==", "dependencies": { "@ably/msgpack-js": "^0.4.0", + "fastestsmallesttextencoderdecoder": "^1.0.22", "got": "^11.8.5", "ws": "^8.14.2" }, "engines": { - "node": ">=5.10.x" + "node": ">=16" }, "peerDependencies": { "react": ">=16.8.0", @@ -1868,6 +1869,11 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fastestsmallesttextencoderdecoder": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", + "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", diff --git a/package.json b/package.json index 3be08cc..0821daa 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "format:check": "prettier --check --ignore-path .gitignore app components" }, "dependencies": { - "ably": "^1.2.44", + "ably": "^2.0.4", "next": "^13.4.19", "react": "^18.2.0", "react-dom": "^18.2.0" From 22c6f6bf24ac7cdf8bf3ec06273ef5db2ab631ca Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Fri, 17 May 2024 09:43:58 +0100 Subject: [PATCH 2/5] Migrate to using ably-js v2 --- app/api/route.js | 14 ++++++++++---- components/Chat.jsx | 8 +++++--- components/ChatBox.jsx | 2 -- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/api/route.js b/app/api/route.js index 3173bc0..3a19986 100644 --- a/app/api/route.js +++ b/app/api/route.js @@ -1,9 +1,15 @@ -import Ably from 'ably/promises'; +import Ably from 'ably'; -export async function GET(request) { - const client = new Ably.Realtime(process.env.ABLY_API_KEY); - const tokenRequestData = await client.auth.createTokenRequest({ clientId: 'ably-nextjs-demo' }); +// ensure Vercel doesn't cache the result of this route, +// as otherwise the token request data will eventually become outdated +// and we won't be able to authenticate on client side +export const revalidate = 0; +export async function GET(request) { + const client = new Ably.Rest(process.env.ABLY_API_KEY); + const tokenRequestData = await client.auth.createTokenRequest({ + clientId: 'ably-nextjs-demo', + }); console.log(`Request: ${JSON.stringify(tokenRequestData)}`); return Response.json(tokenRequestData); } diff --git a/components/Chat.jsx b/components/Chat.jsx index 6d7fb70..d5ded85 100644 --- a/components/Chat.jsx +++ b/components/Chat.jsx @@ -1,15 +1,17 @@ 'use client'; import * as Ably from 'ably'; -import { AblyProvider } from 'ably/react'; +import { AblyProvider, ChannelProvider } from 'ably/react'; import ChatBox from './ChatBox.jsx'; export default function Chat() { - const client = Ably.Realtime.Promise({ authUrl: '/api' }); + const client = new Ably.Realtime({ authUrl: '/api' }); return ( - + + + ); } diff --git a/components/ChatBox.jsx b/components/ChatBox.jsx index 9926bd4..5a1d6ea 100644 --- a/components/ChatBox.jsx +++ b/components/ChatBox.jsx @@ -1,5 +1,3 @@ -'use client'; - import React, { useEffect, useState } from 'react'; import { useChannel } from 'ably/react'; import styles from './ChatBox.module.css'; From a0fe5577148e4264d0294965fc35e7d86389e927 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Fri, 17 May 2024 09:45:14 +0100 Subject: [PATCH 3/5] Fix incorrect `globals.css` import Resolves #8 --- app/layout.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/layout.js b/app/layout.js index 11cf057..6e26cc1 100644 --- a/app/layout.js +++ b/app/layout.js @@ -1,4 +1,4 @@ -import './global.css'; +import './globals.css'; export const metadata = { title: 'Next.js', From b0989d996e962d8db3ebf61f1629c171b656dc8d Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Fri, 17 May 2024 10:30:48 +0100 Subject: [PATCH 4/5] Fix `Critical dependency: the request of a dependency is an expression` The same fix as in https://github.com/ably-labs/ably-nextjs-fundamentals-kit/pull/17 --- next.config.mjs | 8 +++ package-lock.json | 131 ++++++++++++++++++++++------------------------ package.json | 2 +- 3 files changed, 71 insertions(+), 70 deletions(-) create mode 100644 next.config.mjs diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..c6cd3a7 --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + experimental: { + serverComponentsExternalPackages: ['ably'], + }, +}; + +export default nextConfig; diff --git a/package-lock.json b/package-lock.json index d4f5f29..dccec67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "ably": "^2.0.4", - "next": "^13.4.19", + "next": "^14.2.3", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -129,9 +129,9 @@ "dev": true }, "node_modules/@next/env": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.6.tgz", - "integrity": "sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==" + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz", + "integrity": "sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==" }, "node_modules/@next/eslint-plugin-next": { "version": "13.5.6", @@ -143,9 +143,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.6.tgz", - "integrity": "sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.3.tgz", + "integrity": "sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==", "cpu": [ "arm64" ], @@ -158,9 +158,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.6.tgz", - "integrity": "sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz", + "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==", "cpu": [ "x64" ], @@ -173,9 +173,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.6.tgz", - "integrity": "sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz", + "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==", "cpu": [ "arm64" ], @@ -188,9 +188,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.6.tgz", - "integrity": "sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz", + "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==", "cpu": [ "arm64" ], @@ -203,9 +203,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.6.tgz", - "integrity": "sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz", + "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==", "cpu": [ "x64" ], @@ -218,9 +218,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.6.tgz", - "integrity": "sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz", + "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==", "cpu": [ "x64" ], @@ -233,9 +233,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.6.tgz", - "integrity": "sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz", + "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==", "cpu": [ "arm64" ], @@ -248,9 +248,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.6.tgz", - "integrity": "sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz", + "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==", "cpu": [ "ia32" ], @@ -263,9 +263,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.6.tgz", - "integrity": "sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz", + "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==", "cpu": [ "x64" ], @@ -329,11 +329,17 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + }, "node_modules/@swc/helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", - "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", "dependencies": { + "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, @@ -2088,11 +2094,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" - }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -2946,37 +2947,38 @@ "dev": true }, "node_modules/next": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/next/-/next-13.5.6.tgz", - "integrity": "sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.3.tgz", + "integrity": "sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==", "dependencies": { - "@next/env": "13.5.6", - "@swc/helpers": "0.5.2", + "@next/env": "14.2.3", + "@swc/helpers": "0.5.5", "busboy": "1.6.0", - "caniuse-lite": "^1.0.30001406", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", "postcss": "8.4.31", - "styled-jsx": "5.1.1", - "watchpack": "2.4.0" + "styled-jsx": "5.1.1" }, "bin": { "next": "dist/bin/next" }, "engines": { - "node": ">=16.14.0" + "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "13.5.6", - "@next/swc-darwin-x64": "13.5.6", - "@next/swc-linux-arm64-gnu": "13.5.6", - "@next/swc-linux-arm64-musl": "13.5.6", - "@next/swc-linux-x64-gnu": "13.5.6", - "@next/swc-linux-x64-musl": "13.5.6", - "@next/swc-win32-arm64-msvc": "13.5.6", - "@next/swc-win32-ia32-msvc": "13.5.6", - "@next/swc-win32-x64-msvc": "13.5.6" + "@next/swc-darwin-arm64": "14.2.3", + "@next/swc-darwin-x64": "14.2.3", + "@next/swc-linux-arm64-gnu": "14.2.3", + "@next/swc-linux-arm64-musl": "14.2.3", + "@next/swc-linux-x64-gnu": "14.2.3", + "@next/swc-linux-x64-musl": "14.2.3", + "@next/swc-win32-arm64-msvc": "14.2.3", + "@next/swc-win32-ia32-msvc": "14.2.3", + "@next/swc-win32-x64-msvc": "14.2.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" @@ -2985,6 +2987,9 @@ "@opentelemetry/api": { "optional": true }, + "@playwright/test": { + "optional": true + }, "sass": { "optional": true } @@ -4059,18 +4064,6 @@ "punycode": "^2.1.0" } }, - "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 0821daa..da7f3a8 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "ably": "^2.0.4", - "next": "^13.4.19", + "next": "^14.2.3", "react": "^18.2.0", "react-dom": "^18.2.0" }, From d0b0f00df93f7337c6bc25fcd8772423eb04b844 Mon Sep 17 00:00:00 2001 From: Andrew Bulat Date: Fri, 17 May 2024 12:26:08 +0100 Subject: [PATCH 5/5] Remove obsolete information from README and tidy it up --- README.md | 449 ++++++------------------------------------------------ 1 file changed, 46 insertions(+), 403 deletions(-) diff --git a/README.md b/README.md index fc7d25f..7ddbf8e 100644 --- a/README.md +++ b/README.md @@ -1,431 +1,68 @@ # Building a Realtime Chat App with Next.js, Ably, and Vercel -https://next-js-chat-app.vercel.app/ +Live example at: -This is a demo of building a chat application with [Next.js](https://nextjs.org/) using Ably as the messaging platform. +Step by step guide at: -You'll learn how to - -* Create a brand new Next.js application -* Create an Ably account and get an API key -* Create a Next.js Vercel Serverless API -* Use React Functional components and React Hooks with Ably -* Host your app on Vercel +This is a demo chat application with [Next.js](https://nextjs.org/) using [Ably](https://ably.com) as the messaging platform. -[Next.js](https://nextjs.org/) is a React framework from [Vercel](https://vercel.com/). It is used to build static web applications with server side rendering, serverless functions and seamless hosting. It's a framework that takes the React knowledge you already have, and puts some structure and conventions in place. +## Description -[Ably](https://www.ably.io/) is realtime, pub/sub messaging platform with a suite of integrated services to deliver complete realtime functionality directly to end-users. +It demonstrates the use of: -[Vercel](https://vercel.com/) is a hosting platform, built from the ground up to host Next.js apps, and Serverless Functions with them. +- Pub/sub messaging +- Ably's React Hooks +- Token authentication with Ably -[React](https://reactjs.org/) is a JavaScript library for building user interfaces with encapsulated components that manage their own state. +## Tech stack -# WebSockets in Vercel with Ably +The project uses the following components: -![Vercel and Websockets](https://cdn.glitch.com/0cb30add-c9ef-4c00-983c-e12deb0d4080%2Fvercel-websockets.png?v=1610475709091) +- [Next.js](https://nextjs.org/) is a React framework from [Vercel](https://vercel.com/). It is used to build static web applications with server side rendering, serverless functions and seamless hosting. It's a framework that takes the React knowledge you already have, and puts some structure and conventions in place. -Vercel allows users to deploy [Serverless Functions](https://vercel.com/docs/serverless-functions/introduction), which are essentially just blocks of code which provide a response to an HTTP request. However, these functions have a maximum execution timeout, which means that it is not possible to maintain a WebSocket connection this way. This is where Ably comes in. The client can connect to an [Ably Channel](https://www.ably.io/documentation/realtime/channels) and send and receive messages on it to add Realtime functionality to your app by managing your WebSocket connections for you. We'll go over how to build an app which uses realtime functionality in this walkthrough, if preferred, you can [jump straight to how to use Ably with Vercel](#ablyandvercel). +- [Ably](https://ably.com/) is realtime, pub/sub messaging platform with a suite of integrated services to deliver complete realtime functionality directly to end-users. -# What are we going to build? +- [Vercel](https://vercel.com/) is a hosting platform, built from the ground up to host Next.js apps, and Serverless Functions with them. + +- [React](https://reactjs.org/) is a JavaScript library for building user interfaces with encapsulated components that manage their own state. + +## What are we going to build? ![The UI of the chat app we'll build. It is a window with speech bubbles for text.](https://cdn.glitch.com/0cb30add-c9ef-4c00-983c-e12deb0d4080%2Fchatapp.png?v=1612279601157) *The UI of the app we'll build with this walkthrough* We'll build a realtime chat app that runs in the browser. It will be built upon the Next.js [create-next-app](https://nextjs.org/docs/api-reference/create-next-app) template, it will contain a React component which will use Ably to send and receive messages. We'll also write a Next.js serverless function which will be used to connect to Ably. -## Dependencies +## Building & running locally -In order to build this app, you will need: +### Prerequisites -* **An Ably account** for sending messages: [Create an account with Ably for free](https://www.ably.io/signup). -* **A Vercel Account** for hosting on production: [Create an account with Vercel for free](https://vercel.com/signup). -* **Node 12** (LTS) or greater: [Install Node](https://nodejs.org/en/). +In order to build and deploy this app, you will need: -## Local dev pre-requirements +- **An Ably account** for sending messages: [Create an account with Ably for free](https://ably.com/signup). +- **A Vercel Account** for hosting on production: [Create an account with Vercel for free](https://vercel.com/signup). +- **Node 16** or greater: [Install Node](https://nodejs.org/en/). -You'll need an API key from Ably to authenticate with the Ably Service. To get an API key, once you have [created an Ably account](https://www.ably.io/signup): +You'll also need an API key from Ably to authenticate with the Ably Service. To get an API key, once you have [created an Ably account](https://ably.com/signup): -1. Visit your [app dashboard](https://www.ably.io/accounts/any) and click on "Create New App". -2. Give the new app a name +1. Visit your [app dashboard](https://ably.com/accounts/any) and click on "Create new app". +2. Give the new app a name. 3. Copy the Private API key once the app has been created. Keep it safe, this is how you will authenticate with the Ably service. -Vercel provides some Next.js command line tools to help us. They don't need to be installed on your system as they're executed using `npx`. +### Building the project -# Building the Realtime Chat App -### To create the starter app: +1. To run this project locally, fork this repo and create a file called `.env` in the root of the project containing your Ably API key: -1. In your terminal, type `npx create-next-app` to create an empty Next.js app. -2. Create a file called `.env` in the root of the directory, this is where we'll put the project's environment variables. -3. Add your Ably API key to the .env file: -``` +```sh ABLY_API_KEY=your-ably-api-key:goes-here ``` -4. Navigate to your Next.js application directory and type into the console: - -```bash -npm run dev -``` - -The Next.js dev server will spin up and you'll see an empty Next.JS starter app. This is what we'll build our chat app on top of. - -# Realtime Pub/Sub messaging with Ably - -The chat app we'll build uses [Ably](https://www.ably.io/) for [pub/sub messaging](https://www.ably.io/documentation/core-features/pubsub) between the users. Pub/Sub stands for Publish and Subscribe, and it is a popular pattern used for realtime data delivery. The app will be able to send, or `publish` messages over an [Ably Channel](https://www.ably.io/channels). The clients that use the app will be `subscribed` to the channel and will be able to receive the messages. We'll build a UI to create messages to be sent, and to display messages as they are received. - -## Authentication with the Ably service - -Vercel Next.js apps don't run traditional "server side code", however, you can add JavaScript files to `/pages/api/*` and the Vercel deployment engine will treat each one as an API endpoint and manage them as serverless functions for you. - -For local development, the Next.js tools run these functions in a Node server, so they work as you would expect in your local dev environment. We're going to add a Next.js / Vercel serverless function to the starter code that we created earlier to authenticate our app with Ably, and make it possible to start sending and receiving messages over the Ably service. - -## Writing the Serverless function to connect to Ably - -You'll need to install the [Ably npm package](https://www.npmjs.com/package/ably/v/1.2.5-beta.1) (it's important you're running Ably 1.2.5+ for this app, for compatibility with Vercel). - -In the terminal, in the root of your new app run: - -```bash -npm install ably@1.2.5-beta.1 -``` - -Next, create a file called `./pages/api/createTokenRequest.js` into which add the following code: - -```js -import Ably from "ably/promises"; - -export default async function handler(req, res) { - const client = new Ably.Realtime(process.env.ABLY_API_KEY); - const tokenRequestData = await client.auth.createTokenRequest({ clientId: 'ably-nextjs-demo' }); - res.status(200).json(tokenRequestData); -}; -``` - -This serverless function uses the Ably SDK to create a `tokenRequest` with your API key. This token will be used later - it allows you to keep your "real" API key safe while using it in the Next.js app. By default, this API is configured to be available on `http://localhost:3000/api/createTokenRequest` -We're going to provide this URL to the Ably SDK in our client to authenticate with Ably. - -# The Realtime Chat App Architecture - -The topology of our Next.js app will look like this: - -```bash -├─ .env -├─ .gitignore -├─ package-lock.json -├─ package.json -├─ README.md -| -├─── components -│ ├─ AblyChatComponent.jsx -│ ├─ AblyChatComponent.module.css -│ └─ AblyReactEffect.js -| -├─── pages -│ ├─ index.js -│ │ -│ └─── api -│ └─ createTokenRequest.js -│ -└─── public -``` - -* `/pages/index.js` is the home page -* `/api/createTokenRequest.js` is our Ably token authentication API -* `/components/AblyChatComponent.jsx` is the chat component -* `/components/AblyChatComponent.module.css` contains the styles for the chat component -* `/components/AblyReactEffect.js` is the Ably React Hook. - -Let's walk through how this application is built. - -# Building the Components - -Pages in `Next.js` are React components, so the `pages/index.js` home page is the React component that contains the page layout. - -This is the default page generated by `create-next-app`, we'll add our own component to this - an `AblyChatComponent`: - -```jsx -import Head from 'next/head' -import dynamic from 'next/dynamic' - -const AblyChatComponent = dynamic(() => import('../components/AblyChatComponent'), { ssr: false }); - -export default function Home() { - return ( -
- - Create Next App - - - -
-

Next.js Chat Demo

- -
- - - - - - -
- ) -} - -``` - -You'll notice that it doesn't look like a regular import - we're including it like this: - -```jsx -const AblyChatComponent = dynamic(() => import('../components/AblyChatComponent'), { ssr: false }); -``` - -before using it like any other react component: - -```jsx -
-

Next.js Chat Demo

- -
-``` - -This unusual include style is because the `AblyChatComponent` can only be run inside the user's browser. It connects using WebSockets and isn't suitable for Server Side Rendering with Vercel. By default, Next.js attempts to render everything on the server side, so by including the component using a `dynamic()` call, we can tell Next.js not to render this during the build process, where it would throw errors because it can't connect to the APIs that it needs to function. - -# Writing the Chat Component Logic - -The chat app logic is contained inside the `AblyChatComponent.jsx` component. - -Start off by referencing the imports we'll need at the top of the file: - -```jsx -import React, { useEffect, useState } from 'react'; -import { useChannel } from "./AblyReactEffect"; -import styles from './AblyChatComponent.module.css'; -``` - -Then we'll define the function that will be exported as a React Functional component. We need to access some HTML elements in the code so we can create variables to store their references: - -```jsx -const AblyChatComponent = () => { - - let inputBox = null; - let messageEnd = null; -``` - -Next, set up the state properties that we'll use in the component: - -```jsx - const [messageText, setMessageText] = useState(""); - const [receivedMessages, setMessages] = useState([]); - const messageTextIsEmpty = messageText.trim().length === 0; -``` - -* **messageText** will be bound to textarea element where messages can be typed -* **receiveMessages** to the on screen chat history -* **messageTextIsEmpty** is used to disable the send button when the textarea is empty - -Now we'll make use of the `useChannel` hook that we imported earlier. -`useChannel` is a [react-hook](https://reactjs.org/docs/hooks-intro.html) style API for subscribing to messages from an Ably channel. You provide it with a channel name and a callback to be invoked whenever a message is received. +2. Run `npm install`. +3. Run `npm run dev`. -```jsx - const [channel, ably] = useChannel("chat-demo", (message) => { - // Here we're computing the state that'll be drawn into the message history - // We do that by slicing the last 199 messages from the receivedMessages buffer +The Next.js dev server will spin up and you'll see a demo chat application. - const history = receivedMessages.slice(-199); - setMessages([...history, message]); - - // Then finally, we take the message history, and combine it with the new message - // This means we'll always have up to 199 message + 1 new message, stored using the - // setMessages react useState hook - }); -``` - -Next, we need to handle the UI interactions by defining a few functions. - -First, there's `sendChatMessage`, which is responsible for publishing new messages. -It uses the Ably Channel returned by the `useChannel` hook, clears the input, and focuses on the textarea so that users can type more messages: - -```jsx - const sendChatMessage = (messageText) => { - channel.publish({ name: "chat-message", data: messageText }); - setMessageText(""); - inputBox.focus(); - } -``` - -Then `handleFormSubmission`, which is triggered when the `submit` button is clicked and calls `sendChatMessage`, along with preventing a page reload: - -```jsx - const handleFormSubmission = (event) => { - event.preventDefault(); - sendChatMessage(messageText); - } -``` - -In addition, the `handleKeyPress` event is wired up to make sure that if a user presses the `enter` key, while there is text in the textarea, the `sendChatMessage` function is triggered. - -```jsx - const handleKeyPress = (event) => { - if (e.charCode !== 13 || messageTextIsEmpty) { - return; - } - sendChatMessage(messageText); - event.preventDefault(); - } -``` - -Next, we need to construct the UI elements to display the messages. To do this, we will [map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) the received Ably messages into HTML span elements: - -```jsx - const messages = receivedMessages.map((message, index) => { - const author = message.connectionId === ably.connection.id ? "me" : "other"; - return {message.data}; - }); -``` - -In order to keep the message box scrolled to the most recent message (the one on the bottom) we'll need to add an empty div element into the message container, which will then be scrolled into view whenever the components re-renders. This is the element that we'll add to the UI later: - -```jsx -
{ messageEnd = element; }}>
-``` - -We use a `useEffect` hook along with [`scrollIntoView()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) to scroll the message history to the bottom whenever the component renders. - -```jsx - useEffect(() => { - messageEnd.scrollIntoView({ behaviour: "smooth" }); - }); -``` - -Finally we will write the React component markup with the event handlers all bound up to `onChange` and `onKeyPress` events in JSX. - -The markup itself is just a few div elements and a form with a textarea for user input. - -There are two calls to the react `ref` function, which allows us to capture a reference to the elements when they are rendered so that we can interact with them in JavaScript. - -The returned markup will look like this: - -```jsx - return ( -
-
- {messages} -
{ messageEnd = element; }}>
// empty element to control scroll to bottom -
-
- - -
-
- ) -} - -export default AblyChatComponent; -``` - -Right at the bottom of the file, the function is exported as `AblyChatComponent` so that it can be referenced in the Next.js page we created at the start. - -## Using Ably correctly in React Components - -One of the trickier parts of using Ably with React Functional Components is knowing when and where to create the instance of the SDK and when and where to connect to your channel(s). You will want to avoid instancing the SDK when the component is rendered as this could make multiple connections and burn through your Ably account limits. - -To make sure that the app handles component redrawing, mounting and unmounting correctly - `AblyReactEffect` exports a [React Hook](https://reactjs.org/docs/hooks-intro.html) to interact with the Ably SDK. - -React hooks can seem a little unusual the first time you use them. A hook is a function which: - -* Executes the functionality that we'd expect `componentDidMount` to run -* Returns *another* function that will be executed by the framework where `componentDidUnmount` would be called -* Performs any other behaviour it needs to - -This React Hook is built upon `useEffect`. When referenced, it creates an instance of the Ably SDK (it does this only once) which is configured to use the `URL` of your Serverless function to `createTokenRequest` for authentication: - -```js -import Ably from "ably/promises"; -import { useEffect } from 'react' - -const ably = new Ably.Realtime.Promise({ authUrl: '/api/createTokenRequest' }); -``` - -Instancing the Ably library outside the scope of the component will mean it is only created once and will keep your limit usage down. - -We then need to create the function we're going to export - our Hook, so that we can use it in our components. -We'll call it `useChannel` and it will require the channel name, and a callback as arguments. Each time `useChannel` is called, we [`get` the requested channel](https://www.ably.io/documentation/realtime/channels#obtaining-channel) from the Ably-JS SDK and prepare the hook functions. - -* **onMount** is the code run each time our component is rendered. Inside onMount, we will subscribe to the specified channel, triggering `callbackOnMessage` whenever a message is received. -* **onUnmount** is the code run whenever the component is unmounted before it is re-rendered. Here we will unsubscribe from the channel, which will stop accidental multiples of connections, again saving our account limits. -* **useEffectHook** is a function that calls these functions correctly, returning onUnmount for React to use. - -The exported Hook in `AblyReactEffect.js` will look like this: - -```js -export function useChannel(channelName, callbackOnMessage) { - const channel = ably.channels.get(channelName); - - const onMount = () => { - channel.subscribe(msg => { callbackOnMessage(msg); }); - } - - const onUnmount = () => { - channel.unsubscribe(); - } - - const useEffectHook = () => { - onMount(); - return () => { onUnmount(); }; - }; - - useEffect(useEffectHook); - - return [channel, ably]; -} -``` - -The `useChannel` Hook returns both the current Ably channel and the Ably SDK for the calling code to use to send messages. This hook encapsulates Ably pub/sub for React functional components in one place, so we don't need to worry about it elsewhere, and the code that uses it can just process the messages it receives. - -## Making everything look beautiful with module CSS - `AblyChatComponent.module.css` - -You might have noticed when writing the chat component that `Next.js` has some compiler enforced conventions that dictate where you keep your CSS and how to import it. -For this app, we will create a CSS file with the same name as the `.jsx` file, just with the extensions `.module.css`. We do this to keep management of the components easier, if in the future we want to delete this component it is nice and simple to also remove its CSS. Once created, it can be imported into the component: - -```js -import styles from './AblyChatComponent.module.css'; -``` - -When creating a CSS class on a JSX element, we use the following syntax on the element: - -```js - className={styles.yourClassName} -``` - -and the accompanying css would look like this: - -```css -.yourClassName { - styles: gohere; -} -``` -This app is built with [CSS Grid](https://css-tricks.com/snippets/css/complete-guide-grid/) to create the app layout, you are of course welcome to use the CSS provided with this project or to write your own or use a framework. - -# Hosting on Vercel +## Hosting on Vercel We're using `Vercel` as our development server and build pipeline. @@ -435,27 +72,33 @@ We're using `Vercel` as our development server and build pipeline. In order to deploy your new chat app to Vercel you'll need to: 1. Create a [GitHub account](https://github.com/) (if you don't already have one) -2. [Push your app to a GitHub repository](https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/creating-a-new-repository) +2. [Push your app to a GitHub repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-new-repository) 3. [Create a Vercel account](https://vercel.com/signup) 4. Create a new Vercel app and import your app from your GitHub repository. (This will require you to authorise Vercel to use your GitHub account) 5. Add your `ABLY_API_KEY` as an environment variable 6. Watch your app deploy 7. Visit the newly created URL in your browser! -# Make it your own +## Make it your own There are a few ways that this example could be extended: -## Add message history +### Add message history -There is currently no chat history in this demo, you'll only see messages that come in after you join the chat. You could expand this demo by using [Ably's rewind feature](https://www.ably.io/documentation/realtime/history) for up to two minutes of history for free, or with a paid account, for up to ~48 hours. +There is currently no chat history in this demo, you'll only see messages that come in after you join the chat. You could expand this demo by using [Ably's rewind feature](https://ably.com/docs/storage-history/history) for up to two minutes of history for free, or with a paid account, for up to ~48 hours. -## Add user names +### Add user names There aren't any usernames sent with the chat messages. This demo could be extended to introduce a username input box, and to add the current username to messages as they're sent. The demo uses the randomly generated Ably client Id as a unique identifier - which is how it can detect if it is "me" or "someone else" who sent the message. -# Let us Know +## More info + +- [Join our Discord server](https://discord.gg/q89gDHZcBK) +- [Follow us on Twitter](https://twitter.com/ablyrealtime) +- [Use our SDKs](https://github.com/ably/) +- [Visit our website](https://ably.com) -If this tutorial was helpful, or you're using Next.js and Ably in your project, we'd love to hear about it. Drop us a [message on Twitter](https://twitter.com/ablyrealtime) or email us at [devrel@ably.io](mailto:devrel@ably.io). +--- +[![Ably logo](https://static.ably.dev/badge-black.svg?ably-next-vercel-news)](https://ably.com)