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.
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:
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
:
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
)
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>
)
}
}
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)
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
)
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>
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:
Now navigate to the SQL tab, where we will create our two tables Lists
and Tasks
using the built-in SQL interpreter:
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
);
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)
}
}
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()
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.
For the TodoList component we will start by importing from the store:
import { useStore, addTask, updateTask } from './Store'
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>
)
}
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
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)
supabase is really awesome