DEV Community

Cover image for HowTo: Build Collaborative Realtime Task Lists in React
awalias
awalias

Posted on • Updated on

HowTo: Build Collaborative Realtime Task Lists in React

To commiserate the shutting down of Wunderlist I thought that today we could learn how to build this - https://todo-zeta.now.sh/ - a simple, collaborative and realtime task list service. Users can create a new list and share it with friends/colleagues to complete together.

Alt Text

We're going to use functional React on the front end, and Supabase as our database and realtime engine (full disclosure: I'm a co-founder of Supabase). (what is supabase?)

If you want to skip ahead, you can find the final source code here: https://github.com/supabase/supabase/tree/master/examples/react-todo-list

otherwise let's dive in...

1) Create your project base

For this I used create-react-app npx create-react-app my-todo-app

Then go ahead and re-structure your project to look like this:

Alt Text

index.js will be our entry point where we create new lists, TodoList.js will be the list we create, and we'll be fetching all of our data from Store.js.

Then add in these dependencies into package.json:

Alt Text

and install them all by running npm install

2) index.js

Add in our base Router with the render function:

import { render } from 'react-dom'

render(
  <div className="App">
    <Router>
      <Switch>
        <Route exact path="/" component={Home} />
        {/* Additional Routes go here */}
      </Switch>
    </Router>
  </div>,
  document.body
)
Enter fullscreen mode Exit fullscreen mode

next you'll want to set up your main component:

const newList = async (history) => {
  const list = await createList(uuidv4())
  history.push(`/?uuid=${list.uuid}`)
}

const Home = (props) => {
  const history = useHistory()
  const uuid = queryString.parse(props.location.search).uuid

  if (uuid) return TodoList(uuid)
  else {
    return (
      <div className="container">
        <div className="section">
          <h1>Collaborative Task Lists</h1>
          <small>
            Powered by <a href="https://supabase.io">Supabase</a>
          </small>
        </div>
        <div className="section">
          <button
            onClick={() => {
              newList(history)
            }}
          >
            new task list
          </button>
        </div>
      </div>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

The key part here is that when the create list button is clicked, we createList(uuidv4()) with a randomly generated uuid, and then we append it to the current url as a query parameter using useHistory() and history.push(...). We do this so that the user can copy and share the url from the url bar.

Also then, when a new user receives an url from their friend - the app knows to look up the specific task list from the db using the given uuid, you can see this here:

  const uuid = queryString.parse(props.location.search).uuid
  if (uuid) return TodoList(uuid)
Enter fullscreen mode Exit fullscreen mode

index.js <- I have omitted some of the boring code so grab the rest from here to finish your index file.

3) Store.js

Now we'll look at how to set, fetch, and listen to your data in realtime so that you can show new and completed tasks to collaborating user's without them having to refresh the page.

import { useState, useEffect } from 'react'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.REACT_APP_SUPABASE_URL,
  process.env.REACT_APP_SUPABASE_KEY
)
Enter fullscreen mode Exit fullscreen mode

You will need a .env file in the project root where we store these variables:

REACT_APP_SUPABASE_URL=<my-url>
REACT_APP_SUPABASE_KEY=<my-key>
Enter fullscreen mode Exit fullscreen mode

To get your Supabase credentials, go to app.supabase.io, create a new organisation and project, and navigate to the API page where you will find your keys:

Alt Text

Now navigate to the SQL tab, where we will create our two tables Lists and Tasks using the built-in SQL interpreter:

Alt Text

run these two queries to create the tables:

CREATE TABLE lists (
  uuid text,
  id bigserial PRIMARY KEY,
  inserted_at timestamp without time zone DEFAULT timezone('utc' :: text, now()) NOT NULL,
  updated_at timestamp without time zone DEFAULT timezone('utc' :: text, now()) NOT NULL
);

CREATE TABLE tasks (
  task_text text NOT NULL,
  complete boolean DEFAULT false,
  id bigserial PRIMARY KEY,
  list_id bigint REFERENCES lists NOT NULL,
  inserted_at timestamp without time zone DEFAULT timezone('utc' :: text, now()) NOT NULL,
  updated_at timestamp without time zone DEFAULT timezone('utc' :: text, now()) NOT NULL
);
Enter fullscreen mode Exit fullscreen mode

Now, in Store.js, we can fill out the createList method that we called from index.js:

export const createList = async (uuid) => {
  try {
    let { body } = await supabase.from('lists').insert([{ uuid }])
    return body[0]
  } catch (error) {
    console.log('error', error)
  }
}
Enter fullscreen mode Exit fullscreen mode

You can head to Store.js to grab the rest of the code but the other points of note here are:

how we subscribe to realtime changes on your task list:

        supabase
          .from(`tasks:list_id=eq.${list.id}`)
          .on('INSERT', (payload) => handleNewTask(payload.new))
          .on('UPDATE', (payload) => handleNewTask(payload.new))
          .subscribe()
Enter fullscreen mode Exit fullscreen mode

and how we manage the state using useState() and useEffect. This can be a little tricky to get your head around at first so make sure to read over Using the Effect Hook to understand how it all fits together.

4)TodoList.js

For the TodoList component we will start by importing from the store:

import { useStore, addTask, updateTask } from './Store'
Enter fullscreen mode Exit fullscreen mode

and then you can use them like you would any other state variable:

export const TodoList = (uuid) => {
  const [newTaskText, setNewTaskText] = useState('')
  const { tasks, setTasks, list } = useStore({ uuid })

  return (
    <div className="container">
      <Link to="/">back</Link>
      <h1 className="section">My Task List</h1>
      <div className="section">
        <label>Sharing url: </label>
        <input type="text" readonly value={window.location.href} />
      </div>
      <div className={'field-row section'}>
        <form
          onSubmit={(e) => {
            e.preventDefault()
            setNewTaskText('')
          }}
        >
          <input
            id="newtask"
            type="text"
            value={newTaskText}
            onChange={(e) => setNewTaskText(e.target.value)}
          />
          <button type="submit" onClick={() => addTask(newTaskText, list.id)}>
            add task
          </button>
        </form>
      </div>
      <div className="section">
        {tasks
          ? tasks.map((task) => {
              return (
                <div key={task.id} className={'field-row'}>
                  <input
                    checked={task.complete ? true : ''}
                    onChange={(e) => {
                      tasks.find((t, i) => {
                        if (t.id === task.id) {
                          tasks[i].complete = !task.complete
                          return true
                        }
                      })
                      setTasks([...tasks])
                      updateTask(task.id, { complete: e.target.checked })
                    }}
                    type="checkbox"
                    id={`task-${task.id}`}
                  ></input>
                  <label htmlFor={`task-${task.id}`}>
                    {task.complete ? <del>{task.task_text}</del> : task.task_text}
                  </label>
                </div>
              )
            })
          : ''}
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

if you've got everything up and running you should be able to run npm run start and navigate to localhost:3000 to see it in action

Full source code available on github here

Supabase is an Open-Source company & community, so all of our code is available on github.com/supabase

Supabase Docs

Disclaimer: This demo comes without any kind of user authentication, and whilst it's not straight forward to access another user's list you have to work under the assumption that anything you or your users put in their task lists is publicly available information.

Top comments (2)

Collapse
 
rizkyrajitha profile image
Rajitha Gunathilake

supabase is really awesome

Collapse
 
Sloan, the sloth mascot
Comment deleted