Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

A starter repo for a Jamsocket tutorial using NextJS

NotificationsYou must be signed in to change notification settings

jamsocket/jamsocket-nextjs-tutorial

Repository files navigation

This repo is a starter repo for the Jamsocket NextJS + Socket.io Tutorial. The tutorial walks through how to use Jamsocket and Socket.io to add multiplayer presence and state-sharing features to a NextJS whiteboard app.

To try out a completed version of the whiteboard from this tutorial, check out thecompleted branch, runnpm install andnpm run dev. Then runnpx jamsocket dev in a separate terminal tab and visithttp://localhost:3000.

NextJS + Socket.io Tutorial

Session backends are great for document-editing apps, which often load the entire document into memory and apply changes to the in-memory document. (Here, a document may be a text document, spreadsheet, vector graphic, image, or video, etc). For these kinds of applications, the session backend acts as a stateful layer between your client and storage (i.e. a database or blob store). The session backend is a place to...

  • quickly handle edits to the in-memory document
  • persist changes to your storage of choice in the background

This makes it easier to support collaborative editing features and synchronize your document state between several users and a backing store. Using a session backend to update an in-memory document during the user session also lets you use inexpensive blob storage (like S3) when the document is not being edited. (Read more about session backends inour blogpost about them.)

Let's see how this can work by building a whiteboard app with multiplayer editing and presence features. Here's a little preview of what we're building in this tutorial:

jamsocket-nextjs-demo{ width="750px" }

Note: we'll be usingDocker's command-line tool andNodeJS, so get those installed if you haven't already.

Setting up the starter repo

We've put togethera starter repo for this tutorial that contains a NextJS app along with a few helper functions.

git clone https://github.com/jamsocket/jamsocket-nextjs-tutorial.gitcd jamsocket-nextjs-tutorial

In the project, you'll find a typical NextJS directory structure. We'll add code to the following three files:

  • src/app/page.tsx - This is what gets rendered for the/ path. For this tutorial, it'll be a React Server Component. We'll have it be responsible for starting up a session backend on Jamsocket.
  • src/components/Home.tsx - This is the main client-side component for our app. It is rendered by the Server Component insrc/app/page.tsx and will be responsible for the bulk of the app's functionality.
  • src/session-backend - This directory contains the session backend logic. For this demo, the session backend will just be running aSocket.IO server that holds the state of the document in memory and receives/pushes document updates to/from the users who are currently editing it.

This repo also includes some helper components to kickstart our multiplayer demo:

  • src/components/Whiteboard.tsx - This is a simple little whiteboard component that encapsulates the canvas and all the logic for creating and updating shapes from user interactions and for drawing other users' cursors to the screen.
  • src/components/Content.tsx,src/components/Header.tsx - Some helper components for the styled elements of the demo.

Once you've cloned the repo, run:

npm installnpm run dev

Then you should be able to open the app in your browser on localhost with the port shown in the command output (probablyhttp://localhost:3000).

If everything works, you'll notice you can create shapes by clicking and dragging on the page. You can move existing shapes by dragging them.

At this point, you're probably wondering if "whiteboard" isn't overselling it a bit. And you would be right. Implementing a full whiteboard application will be an exercise left to the reader, but, for now, this very limited whiteboard should serve us well as a demo for implementing state sharing and presence features with session backends.

Speaking of state-sharing and presence features - you'll notice that opening the app in another tab gives you a completely blank canvas. Let's see if we can't get this app to share the state of the whiteboard with other tabs.

Writing our session backend

Let's start by adding presence to our application. When another user enters the document, we want to see their cursor on the canvas and their avatar up in corner of the screen.

Since the session backend will be our source of truth for the document state, let's start there.

Insrc/session-backend/index.ts we're already importingsocket.io, starting a WebSocket server on port 8080, and listening for new connections. Let's add some code that keeps track of which users are currently connected and emit an event to all the clients when a user connects or disconnects.

constusers:Set<{id:string;socket:Socket}>=newSet()io.on('connection',(socket:Socket)=>{console.log('New user connected:',socket.id)// store each user's socket connection and user id (socket.id will be the stand in for user id)constnewUser={id:socket.id, socket}users.add(newUser)// send all existing users a 'user-entered' event for the new usersocket.broadcast.emit('user-entered',newUser.id)// send the new user a 'user-entered' event for each existing userfor(constuserofusers){newUser.socket.emit('user-entered',user.id)}// when a user disconnects, delete the user from our set// and broadcast a 'user-exited' event to all the other userssocket.on('disconnect',()=>{users.delete(newUser)socket.broadcast.emit('user-exited',newUser.id)})})

Now that we've got a simple backend written, it's time to shift our focus to the application code for our NextJS project.

We need to do two things:

  1. get our server component to spawn a new backend when someone opens the whiteboard, and
  2. update our client-side logic to connect to the session backend and listen for ouruser-entered anduser-exited WebSocket events.

Spawning our session backend

In our Page component, let's import@jamsocket/server. It contains helper functions that we can use to spawn a session backend. It's important that we spawn from server code as eventually we'll be using an API token here that we want to keep secret, so let's use our React Server Component (src/app/page.tsx). (If you aren't using React Server Components, this could just as easily be done in an API route.)

import'server-only'import{Jamsocket}from'@jamsocket/server'constjamsocket=newJamsocket({dev:true})

When developing locally with the Jamsocket Dev CLI, we can just pass{ dev: true } to theJamsocket constructor. We'll replace this with account and service names and an API token when it comes time to deploy this to Jamsocket. You can see an example inin the@jamsocket/server docs.

The returnedjamsocket instance has aconnect() method that we'll use to get a connection URL for connecting to the our session backend from a browser. It takes a single, optionalconnectRequest argument. TheconnectRequest object allows us to configure a lot of aspects of how the session backend runs. (Our docs have more information aboutconnect() options for the HTTP API.) For now, we will only use one of those options:key. You can learn more about keyshere, but for now it suffices to say that we'll just use a document name. And for this demo, we'll just have one document that everybody edits calledwhiteboard-123.

The result of thejamsocket.connect() function contains aConnection URL that you can use to connect to the session backend, a status URL which returns the current status of the session backend, and some other values like the backend's ID.

Note thatPage is rendered in a server-side component. This ensures that your secrets aren't leaked to the client. Once we receive the spawn result, thePage component will pass that information to theHomeContainer component.

import'server-only'import{Jamsocket}from'@jamsocket/server'constWHITEBOARD_NAME='whiteboard-123'constjamsocket=newJamsocket({dev:true})exportdefaultasyncfunctionPage(){constconnectResponse=awaitjamsocket.connect({key:WHITEBOARD_NAME})return<HomeContainerconnectResponse={connectResponse}/>}
At this point, the typechecker will have some complaints. Let's fix those in the next section

Connecting to our session backend

To connect to our session backend, theHomeContainer component should acceptconnectResponse as props and pass that into theSessionBackendProvider. TheSessionBackendProvider lets us use Jamsocket's React hooks to interact with the session backend.

You will also need theSocketIOProvider to connect to the SocketIO server running in your session backend. TheSocketIOProvider uses theconnection url fromconnectResponse.url to connect to the SocketIO server. TheSocketIOProvider also lets us use Socket.io-specific React hooks in@jamsocket/socketio to send and listen to events. Because@jamsocket/socketio re-exports@jamsocket/react's exports, we can import everything we need from@jamsocket/socketio.

import{SessionBackendProvider,SocketIOProvider}from'@jamsocket/socketio'importtype{ConnectResponse}from'@jamsocket/socketio'exportdefaultfunctionHomeContainer({ connectResponse}:{connectResponse:ConnectResponse}){return(<SessionBackendProviderconnectResponse={connectResponse}><SocketIOProviderurl={connectResponse.url}><Home/></SocketIOProvider></SessionBackendProvider>)}

Next, let's keep track of which users are in the document with some component state. And we can pass that list of users to ourAvatarList component which will render an avatar in the header for each user who is currently in the document.

importtype{Shape,User}from'../types'import{AvatarList}from'./Whiteboard'// ...functionHome(){constready=true// we'll replace this with a real check laterconst[shapes,setShapes]=useState<Shape[]>([])const[users,setUsers]=useState<User[]>([])return(<main><Header><AvatarListusers={users}/></Header><Content>// ...)}

Now, in ourHome component, we can use theuseEventListener hook to listen for ouruser-entered anduser-exited events we're sending from our session backend.

import{SessionBackendProvider,SocketIOProvider,useEventListener}from'@jamsocket/socketio'importtype{ConnectResponse}from'@jamsocket/socketio'

Then we can subscribe to the events with our hook. On theuser-entered event, we should create a user object with anid and acursorX andcursorY property (we'll use these when we implement cursor presence). And on theuser-exited event, let's just remove the user from the list of users in our component state.

functionHome(){constready=true// we'll replace this with a real check laterconst[shapes,setShapes]=useState<Shape[]>([])const[users,setUsers]=useState<User[]>([])useEventListener<string>('user-entered',(id)=>{constnewUser={cursorX:null,cursorY:null, id}setUsers((users)=>[...users,newUser])})useEventListener<string>('user-exited',(id)=>{setUsers((users)=>users.filter((p)=>p.id!==id))})// ...}

Let's also import theuseReady hook that we can use to show a spinner while the session backend is starting up. Depending on your application, it may or may not make sense to show a spinner, but for this demo we'll take the simpler approach of ensuring the session backend is running and the inital document state is loaded before the user can start editing it.

import{SessionBackendProvider,SocketIOProvider,useEventListener,useReady}from'@jamsocket/socketio'importtype{ConnectResponse}from'@jamsocket/socketio'// ...functionHome(){constready=useReady()const[shapes,setShapes]=useState<Shape[]>([])// ...}

Finally - the moment of truth. Let's start the Jamsocket Dev CLI to see if everything works! In another terminal window:

npx jamsocket dev

The dev CLI does several things to make development easier, the first of which is automatically rebuilding our session backend Docker image when the code changes. When you runnpx jamsocket dev, the first thing it does is build your session backend code and start a local server that emulates Jamsocket's API.

Let's take a quick look at thejamsocket.config.json file in the project root to see how all this works:

{"dockerfile":"./src/session-backend/Dockerfile","watch": ["./src/session-backend"],"dockerOptions": {"path":"."  }}

This config file is used by the dev CLI so it knows (1) how to build the session backend into a Docker image and (2) which parts of the file system to watch for changes.

So in our demo, the dev CLI will watch thesrc/session-backend directory, and when a change is detected, it will rebuild the image using the given Dockerfile and the current working directory as the Docker build context. Then, when we refresh the page, thejamsocket.connect() function will send a request to the dev server which will spawn a new backend using the Docker container that was just built and return a connection URL for the backend.

The second thing the dev CLI does for us is keep track of session backends we've spawned during development, terminating backends that are running old code, and streaming status updates and logs from your session backend.

Now with both Jamsocket dev CLI andnpm run dev running in separate terminal windows, you should be able to refresh the page and see an avatar in the header. And if you open the app in another window, another avatar should appear.

If you take a look at the terminal window running the dev CLI, you should see that our server component spawned a backend and now its statuses and logs are appearing in the dev CLI output.

Implementing cursor presence

Most of the hard work is behind us, so let's add a few more events. Let's keep track of the cursor position for each user so we can display that on top of the whiteboard.

We'll start by subscribing to acursor-position event and updating our list of users with the user passed to it:

functionHome(){constready=useReady()const[shapes,setShapes]=useState<Shape[]>([])const[users,setUsers]=useState<User[]>([])useEventListener<User>('cursor-position',(user)=>{setUsers((users)=>users.map((p)=>p.id===user.id ?user :p))})// ...}

Then we need to send acursor-position event to the session backend as our cursor moves over the whiteboard.

We can do this by importing theuseSend hook and then creating asendEvent function with it:

import{SessionBackendProvider,SocketIOProvider,useEventListener,useReady,useSend}from'@jamsocket/socketio'importtype{ConnectResponse}from'@jamsocket/socketio'functionHome(){constready=useReady()constsendEvent=useSend()const[shapes,setShapes]=useState<Shape[]>([])const[users,setUsers]=useState<User[]>([])// ...}

Then, we can pass ausers prop and anonCursorMove prop to our<Whiteboard> component, that takes the cursor's position and sends it to our session backend.

<Whiteboardshapes={shapes}users={users}onCursorMove={(position)=>{sendEvent('cursor-position',{x:position?.x,y:position?.y})}}/>

Now we just need to add acursor-position event to our session backend code. In oursrc/session-backend/index.ts file let's subscribe to thecursor-position event and emit acursor-position event to all connected clients. We can usevolatile.broadcast here because it's okay if we drop a couplecursor-position events here and there. For cursor positions, we really just care about the most recent cursor position message.

io.on('connection',(socket:Socket)=>{console.log('New user connected:',socket.id)socket.on('cursor-position',({ x, y})=>{socket.volatile.broadcast.emit('cursor-position',{id:socket.id,cursorX:x,cursorY:y})})// ...})

Okay, with that, let's take a look at our dev CLI. If it's still running, it should have rebuilt and pushed our session backend code to Jamsocket. It should have also terminated any previous backends running with out of date code.

Now, if we open the application in a new browser window, we should see a new session backend spawning in the dev CLI. If everything works, moving your cursor over one canvas should show a moving cursor on the other client. However, the shapes you create in one window don't appear in the other. Let's fix that in the next section by implementing state sharing across clients.

Implementing shared state

The last thing we want to do in this demo is implement state-sharing. Right now, when you refresh the page, you lose all the shapes you've drawn. And when another connected client draws shapes, you can't see them. Let's fix that.

This time, we'll start with our session backend code. Let's create an array to store all the shapes. When a new user connects, we'll send them a snapshot of all the shapes. Let's also listen for two new events:create-shape andupdate-shape, which will update our list of shapes accordingly.

importtype{Shape}from'../types'// ...constshapes:Shape[]=[]io.on('connection',(socket:Socket)=>{console.log('New user connected:',socket.id)socket.emit('snapshot',shapes)socket.on('cursor-position',({ x, y})=>{socket.volatile.broadcast.emit('cursor-position',{id:socket.id,cursorX:x,cursorY:y})})socket.on('create-shape',(shape:Shape)=>{shapes.push(shape)socket.broadcast.emit('snapshot',shapes)})socket.on('update-shape',(updatedShape:Shape)=>{constshape=shapes.find(s=>s.id===updatedShape.id)if(!shape)returnshape.x=updatedShape.xshape.y=updatedShape.yshape.w=updatedShape.wshape.h=updatedShape.hsocket.broadcast.emit('update-shape',shape)})})

Now, let's add oursendEvent() anduseEventListener() calls to theHome component.

First, we should listen for our newsnapshot andupdate-shape events:

functionHome(){constready=useReady()const[shapes,setShapes]=useState<Shape[]>([])const[users,setUsers]=useState<User[]>([])useEventListener<Shape[]>('snapshot',(shapes)=>{setShapes(shapes)})useEventListener<Shape>('update-shape',(shape)=>{setShapes((shapes)=>{constshapeToUpdate=shapes.find((s)=>s.id===shape.id)if(!shapeToUpdate)return[...shapes,shape]returnshapes.map((s)=>s.id===shape.id ?{ ...s, ...shape} :s)})})// ...}

Then in ouronCreateShape andonUpdateShape Whiteboard props, we should send the appropriate event to the session backend:

<Whiteboardshapes={shapes}users={users}onCursorMove={(position)=>{sendEvent('cursor-position',{x:position?.x,y:position?.y})}}onCreateShape={(shape)=>{sendEvent('create-shape',shape)setShapes([...shapes,shape])}}onUpdateShape={(id,shape)=>{sendEvent('update-shape',{ id, ...shape})setShapes((shapes)=>shapes.map((s)=>s.id===id ?{ ...s, ...shape} :s))}}/>

Now, the dev CLI should have rebuilt the session backend docker image and removed old session backends we had spawned with the previous version of the code. We should be able to simply open the app in a few browser windows and see:

  • an avatar for each user
  • each user's cursor as it hovers over the whiteboard
  • all the same shapes as they are created and moved around the screen

Deploying your session backend code to Jamsocket

The last thing you might want to do is actually run your session backends on Jamsocket. To do that, you'll need to:

  1. Create a Jamsocket account by going toapp.jamsocket.com.
  2. Log in to the Jamsocket CLI and create a service, let's call itwhiteboard-demo:
npx jamsocket loginnpx jamsocket service create whiteboard-demo
  1. Build and push your session backend to Jamsocket:
npx jamsocket push whiteboard-demo -f src/session-backend/Dockerfile
  1. Create an API token onthe Jamsocket settings page.
  2. Change thenew Jamsocket() call insrc/app/page.tsx by passing inaccount,service, andtoken:
constjamsocket=newJamsocket({account:'[YOUR ACCOUNT NAME]',// if you are unsure, you can find this at https://app.jamsocket.com/settingsservice:'whiteboard-demo',token:'[YOUR API TOKEN]',// this is the token you just created in step 5})

Now, when you run your NextJS app locally, it'll spawn your session backend on Jamsocket. You can see which session backends have been spawned by visiting your newwhiteboard-demo service inthe Jamsocket Dashboard or by running:

npx jamsocket backend list

What's next?

If you have any questions about how to use Jamsocket or would like to talk through your particular use case, we'd love to chat! Send us an email athi@jamsocket.com!

About

A starter repo for a Jamsocket tutorial using NextJS

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

[8]ページ先頭

©2009-2025 Movatter.jp