HowTo: Build Collaborative Realtime Task Lists in React
Tags: react, javascript, supabase, webdev
Published: 2020-05-11 05:11:57

To commiserate the shutting down of [Wunderlist](https://techcrunch.com/2019/12/09/microsoft-to-finally-shut-down-to-do-list-app-wunderlist-on-may-6-2020/) I thought that today we could learn how to build [this - https://todo-zeta.now.sh/](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](https://dev-to-uploads.s3.amazonaws.com/i/n48hnd9zl78a0uq68xak.png)

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

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](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](https://reactjs.org/docs/create-a-new-react-app.html) `npx create-react-app my-todo-app`

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/lyhvtwkmooypxow1tccq.png)

`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](https://dev-to-uploads.s3.amazonaws.com/i/336rnaetnbxm9cigoua7.png)

and install them all by running `npm install`

2) [index.js](https://github.com/supabase/supabase/blob/master/examples/react-todo-list/src/index.js)

Add in our base Router with the render function:

```jsx
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
)
``` 

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

```jsx
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>
    )
  }
}
```

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:

```jsx
  const uuid = queryString.parse(props.location.search).uuid
  if (uuid) return TodoList(uuid)
```

[index.js](https://github.com/supabase/supabase/blob/master/examples/react-todo-list/src/index.js) <- I have omitted some of the boring code so grab the rest from here to finish your index file.

3) [Store.js](https://github.com/supabase/supabase/blob/master/examples/react-todo-list/src/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.

```jsx
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
)
```

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

```bash
REACT_APP_SUPABASE_URL=<my-url>
REACT_APP_SUPABASE_KEY=<my-key>
```

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/13fzi1zxs3unuwkeq06l.png)

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

 ![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/0cvfa2r7x3q3931r900h.png)

run these two queries to create the tables:

```sql
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
);
```

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

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

You can head to [`Store.js`](https://github.com/supabase/supabase/blob/master/examples/react-todo-list/src/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:
```jsx
        supabase
          .from(`tasks:list_id=eq.${list.id}`)
          .on('INSERT', (payload) => handleNewTask(payload.new))
          .on('UPDATE', (payload) => handleNewTask(payload.new))
          .subscribe()
```

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](https://reactjs.org/docs/hooks-effect.html) to understand how it all fits together.

4)[TodoList.js](https://github.com/supabase/supabase/blob/master/examples/react-todo-list/src/TodoList.js)

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

```jsx
import { useStore, addTask, updateTask } from './Store'
```

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

```jsx
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>
  )
}
```

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](https://github.com/supabase/supabase/blob/master/examples/react-todo-list)

[Supabase](https://supabase.io) is an Open-Source company & community, so all of our code is available on [github.com/supabase](https://github.com/supabase)

[Supabase Docs](https://supabase.io/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.