Tutorials
To Do List (Next.js)

Making a collaborative To-Do List

Create and edit items in a collaborative to do list. As users edit the list, changes will be automatically persisted and synced in real-time across clients.

Your GIF

Getting Started

Create a NextJS app and install y-sweet.

npx create-next-app@latest todolist --ts --tailwind
cd todolist
npm install @y-sweet/sdk @y-sweet/react

Run your app

npm run dev

Enable state synchronization and persistence

Get a quickstart connection string

If you haven't already, copy your quickstart connection string from the y-sweet dashboard (opens in a new tab). This quickstart connection string is used to connect your app to the y-sweet server that handles state sync and persistence.

getOrCreateDocAndToken

Replace the contents of src/page.tsx with the following. Pass your quickstart connection string key to getOrCreateDocAndToken. Change the definition of CONNECTION_STRING to include your personal “quickstart” connection string.

This connection string is then passed to YDocProvider, which creates a client-side websocket connection. This is how the client speaks to the y-sweet server.

page.tsx
import { ToDoList } from './ToDoList'
import { YDocProvider } from '@y-sweet/react'
import { getOrCreateDocAndToken } from '@y-sweet/sdk'
 
type HomeProps = {
  // searchParams is an object provided by Next.js containing URL parameters.
  // See: https://nextjs.org/docs/app/api-reference/file-conventions/page
  searchParams: Record<string, string>
}
 
// ****************************************************************
// ** TO DO: Replace this with your quickstart connection string **
// ****************************************************************
// For simplicity, we are hard-coding the connection string in the
// file. In a real app, you should instead pass this in through a
// secret store or environment variable.
const CONNECTION_STRING = "[paste your connection string]"
 
export default async function Home({ searchParams }: HomeProps) {
  const clientToken = await getOrCreateDocAndToken(CONNECTION_STRING, searchParams.doc)
 
  return (
    <YDocProvider clientToken={clientToken} setQueryParam="doc">
      <ToDoList />
    </YDocProvider>
  )
}

Note that you'll see errors when you run this code, because we haven't defined ToDoList. We'll do that in the next section.

The To Do List

Create a ToDoList component

In a new file called ToDoList.tsx, create your to do list component.

Note that useArray (opens in a new tab) returns a Y.Array (opens in a new tab), which is like a JavaScript array that is automatically synchronized across clients.

ToDoList.tsx
'use client'
 
import { useArray } from '@y-sweet/react'
import * as Y from 'yjs'
 
export function ToDoList() {
  // Initialize our To Do List as an array.
  // `useArray` returns a Y.Array and also subscribes to changes,
  // so that ToDoList is rerendered when the array changes.
  const items = useArray<Y.Map<any>>('todolist')
 
  return (
    <div className="m-10">
        <div>To Do List</div>
        <div className="space-y-1">
            {/* This line of code won't work yet, but we'll make a ToDoItem
                in the next section  */}
            {items && items.map((item, index) => <ToDoItem key={index} item={item} />)}
        </div>
    </div>
  )
}

Render items with ToDoItem

In ToDoList, we map the array of items as a ToDoItem. Our ToDoItem component should do two things

  1. Render the to do text as a changeable input
  2. Show a checkmark, which indicates whether the item has been completed
ToDoList.tsx
// Put this at the top, with your other imports.
import { useState } from 'react'
 
// Keep the ToDoList implementation from above.
// export function ToDoList() {
//     [...]
// }
 
type ToDoItemProps = {
  item: Y.Map<any>
}
 
export function ToDoItem({ item }: ToDoItemProps) {
  // Yjs has documentation for how to use its shared data types:
  // https://docs.yjs.dev/api/shared-types/y.map
  // For Y.Map, we can get and set values like so:
  const onCompleted = () => {
    item.set('done', !item.get('done'))
  }
 
  return (
    <div>
      <label className="flex flex-row space-x-2 items-center">
        <input
          type="checkbox"
          className="w-6 h-6 cursor-pointer"
          checked={item.get('done')}
          onChange={onCompleted}
        />
        <input
          className="bg-transparent p-1 rounded text-lg"
          value={item.get('text')}
          onChange={(e) => item.set('text', e.target.value)}
        />
      </label>
    </div>
  )
}

Adding items to your To Do List

In ToDoItems, we'll create a function that pushes a new item to the list of to do items. Then we'll pass that function to a a component we'll create called ToDoInput.

ToDoList.tsx
'use client'
 
import { useArray } from '@y-sweet/react'
import * as Y from 'yjs'
 
export function ToDoList() {
  const items = useArray<Y.Map<any>>('todolist')
 
  const pushItem = (text: string) => {
      let item = new Y.Map([
        ['text', text],
        ['done', false],
      ] as [string, any][])
 
      items?.push([item])
  }
 
  return (
    <div className="space-y-1">
        {toDoItems && toDoItems.map((item, index) => <ToDoItem key={index} item={item} />)}
        {/* This line of code won't work yet, but we'll make a ToDoInput in the next section. */}
        <ToDoInput onCreateItem={pushItem} />
    </div>
  )
}

Create the ToDoInput component

In ToDoInput, we'll create a form with a text input and a button. When the form is submitted, we'll create a new item by calling props.onCreateItem.

export function ToDoInput(props: { onCreateItem: (text: string) => void }) {
  const [text, setText] = useState('')
 
  const onSubmit = (e: React.FormEvent) => {
      e.preventDefault()
      props.onCreateItem(text)
      setText('')
    }
 
  return (
    <form onSubmit={onSubmit} className="flex flex-row space-x-2 max-w-2xl">
        <input
          type="text"
          value={text}
          onChange={(e) => setText(e.target.value)}
          className="flex-1 block ring-black rounded-md border-0 px-3.5 py-2 text-gray-900 ring-1 ring-inset placeholder:text-gray-400"
        />
        <button
          type="submit"
          className="block rounded-md bg-blue-600 px-3.5 py-2.5 text-center text-sm hover:bg-blue-500"
        >
          Add
        </button>
    </form>
  )
}

Clear completed items

Your app is now be in a runnable state - if you’re feeling impatient, jump to the next section to try it out.

In the ToDoList component, we'll add a button to clear completed items.

ToDoList.tsx
export function ToDoList() {
  ...
 
  const clearCompletedItems = () => {
    let indexOffset = 0
    items?.forEach((item, index) => {
      if (item.get('done')) {
        items.delete(index - indexOffset, 1)
        indexOffset += 1
      }
    })
  }
 
  return (
      <div className="space-y-1">
        // ...
        <button
            onClick={clearCompletedItems}
            className="block rounded-md bg-blue-600 px-3.5 py-2.5 text-center text-sm hover:bg-blue-500"
        >
            Clear Completed
        </button>
      </div>
  )
}

Run the app

We have enough here to test the to do list.

Run npm run dev and navigate to localhost:3000 (opens in a new tab) or whichever port you're running on.

When you load the page, you'll notice a doc appended to the url. This is a doc automatically created with getOrCreateDocAndToken. If this ID is supplied in the url, the same doc will appear. Otherwise, a new doc will be created.

To see multiplayer in action, copy the URL (including the ?doc=... part), and then open a new window and paste the URL. The to-do list will be synchronized across both windows in real time.

Using the debugger

In the developer tools, you'll find a link to the Y-Sweet Debugger. You can use it to inspect the state of your app.

Your GIF

How to open developer tools:

Next Steps

Refer to our To Do List demo

You can see all the code in action in our to do list demo (opens in a new tab) and style your To Do List to match the cover image.

y-sweet was created by Drifting in Space.