Build a Todo Application
Learn CRUD (Create, Read, Update, Delete) operations in GUN by building a todo application. This tutorial covers data persistence, real-time sync, and state management.What You’ll Learn
- Create items with
.set()and.put() - Read data with
.on()and.once() - Update existing items
- Delete items by setting to
null - Real-time synchronization
- Local storage integration
Minimal Todo App
Here’s a complete todo app in vanilla JavaScript:<!DOCTYPE html>
<html>
<head>
<title>GUN Todo</title>
<style>
body {
font-family: system-ui, sans-serif;
max-width: 600px;
margin: 40px auto;
padding: 20px;
}
h1 {
color: #333;
text-align: center;
}
form {
display: flex;
margin-bottom: 20px;
gap: 10px;
}
input {
flex: 1;
padding: 10px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 16px;
}
button {
padding: 10px 20px;
background: #0066cc;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background: #0052a3;
}
ul {
list-style: none;
padding: 0;
}
li {
padding: 15px;
background: #f9f9f9;
border: 1px solid #ddd;
border-radius: 5px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
li:hover {
background: #f0f0f0;
}
.delete-btn {
background: #dc3545;
padding: 5px 10px;
font-size: 14px;
}
.delete-btn:hover {
background: #c82333;
}
</style>
</head>
<body>
<h1>Todo List</h1>
<form id="form">
<input id="input" placeholder="What needs to be done?" required>
<button type="submit">Add</button>
</form>
<ul id="parentList"></ul>
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
<script>
// Initialize GUN with local and remote storage
const gun = Gun([
'http://localhost:8765/gun',
'https://gun-manhattan.herokuapp.com/gun'
]);
// Get todos reference
const todos = gun.get('todos');
// Get DOM elements
const parentList = document.getElementById('parentList');
const input = document.getElementById('input');
const form = document.getElementById('form');
// Add new todo
form.addEventListener('submit', (e) => {
e.preventDefault();
const todoText = input.value.trim();
if (!todoText) return;
// Add to GUN
todos.set(todoText);
// Clear input
input.value = '';
input.focus();
});
// Delete todo
const deleteTodo = (id) => {
todos.get(id).put(null);
const element = document.getElementById(id);
if (element) {
element.style.display = 'none';
}
};
// Listen for todos
todos.map().on((todo, id) => {
// Skip if null or already exists
if (!todo || document.getElementById(id)) return;
// Create list item
const li = document.createElement('li');
li.id = id;
// Create text span
const span = document.createElement('span');
span.textContent = todo;
// Create delete button
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.className = 'delete-btn';
deleteBtn.onclick = (e) => {
e.stopPropagation();
deleteTodo(id);
};
// Append to list item
li.appendChild(span);
li.appendChild(deleteBtn);
// Add to list
parentList.appendChild(li);
});
</script>
</body>
</html>
React Todo Component
Here’s a more sophisticated React implementation:import React, { Component } from 'react';
import Gun from 'gun/gun';
import 'gun/lib/path';
import './style.css';
const formatTodos = todos => Object.keys(todos)
.map(key => ({ key, val: todos[key] }))
.filter(t => Boolean(t.val) && t.key !== '_');
export default class Todos extends Component {
constructor({ gun }) {
super();
this.gun = gun.get('todos');
this.state = { newTodo: '', todos: [] };
}
componentWillMount() {
this.gun.on(todos => this.setState({
todos: formatTodos(todos)
}));
}
add = e => {
e.preventDefault();
if (!this.state.newTodo.trim()) return;
this.gun.path(Gun.text.random()).put(this.state.newTodo);
this.setState({ newTodo: '' });
}
del = key => this.gun.path(key).put(null)
handleChange = e => this.setState({ newTodo: e.target.value })
render() {
return (
<div className="todos-container">
<h1>My Todos</h1>
<form onSubmit={this.add}>
<input
value={this.state.newTodo}
onChange={this.handleChange}
placeholder="What needs to be done?"
/>
<button onClick={this.add}>Add</button>
</form>
<ul>
{this.state.todos.map(todo =>
<li key={todo.key} onClick={() => this.del(todo.key)}>
{todo.val}
</li>
)}
</ul>
</div>
);
}
}
// Usage:
// const gun = Gun();
// <Todos gun={gun} />
React Hooks Version
Modern React implementation using hooks:import React, { useState, useEffect, useRef } from 'react';
import Gun from 'gun';
const App = () => {
const gun = useRef(Gun()).current;
const newTodo = useRef();
const [todos, setTodos] = useState({});
useEffect(() => {
return gun
.get("todos")
.map()
.on((todo, id) => setTodos(todos => ({ ...todos, [id]: todo }))).off;
}, []);
const addTodo = (e) => {
e.preventDefault();
const title = newTodo.current.value.trim();
if (!title) return;
gun.get("todos").set({ title });
newTodo.current.value = '';
};
const deleteTodo = (id) => {
gun.get("todos").get(id).put(null);
};
return (
<div>
<h1>Todos</h1>
<ul>
{Object.entries(todos)
.filter(([, todo]) => todo && todo.title)
.map(([id, { title }]) => (
<li key={id}>
<span>{title}</span>
<button onClick={() => deleteTodo(id)}>Delete</button>
</li>
))}
</ul>
<form onSubmit={addTodo}>
<input ref={newTodo} placeholder="New todo" />
<button type="submit">Add</button>
</form>
</div>
);
};
export default App;
Advanced Todo with Completion Status
<!DOCTYPE html>
<html>
<head>
<title>Advanced Todo</title>
<style>
.completed {
text-decoration: line-through;
opacity: 0.6;
}
.todo-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
margin-bottom: 10px;
}
input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
}
.todo-text {
flex: 1;
}
</style>
</head>
<body>
<h1>Advanced Todo List</h1>
<form id="add-form">
<input id="new-todo" placeholder="What needs to be done?" required>
<button type="submit">Add</button>
</form>
<div>
<button id="show-all">All</button>
<button id="show-active">Active</button>
<button id="show-completed">Completed</button>
</div>
<ul id="todo-list"></ul>
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
<script>
const gun = Gun();
const todos = gun.get('todos-advanced');
const newTodoInput = document.getElementById('new-todo');
const addForm = document.getElementById('add-form');
const todoList = document.getElementById('todo-list');
let filter = 'all'; // 'all', 'active', 'completed'
// Add new todo
addForm.addEventListener('submit', (e) => {
e.preventDefault();
const text = newTodoInput.value.trim();
if (!text) return;
const todo = {
text: text,
completed: false,
createdAt: Gun.state()
};
todos.set(todo);
newTodoInput.value = '';
});
// Toggle completion
function toggleTodo(id, currentState) {
todos.get(id).get('completed').put(!currentState);
}
// Delete todo
function deleteTodo(id) {
todos.get(id).put(null);
document.getElementById(id)?.remove();
}
// Render todo item
function renderTodo(todo, id) {
if (!todo || !todo.text) return;
// Apply filter
if (filter === 'active' && todo.completed) return;
if (filter === 'completed' && !todo.completed) return;
// Check if exists
let li = document.getElementById(id);
if (!li) {
li = document.createElement('li');
li.id = id;
li.className = 'todo-item';
todoList.appendChild(li);
}
// Update content
li.innerHTML = `
<input type="checkbox" ${todo.completed ? 'checked' : ''}
onchange="toggleTodo('${id}', ${todo.completed})">
<span class="todo-text ${todo.completed ? 'completed' : ''}">
${escapeHtml(todo.text)}
</span>
<button onclick="deleteTodo('${id}')">Delete</button>
`;
}
// Listen for todos
todos.map().on((todo, id) => {
renderTodo(todo, id);
});
// Filter buttons
document.getElementById('show-all').addEventListener('click', () => {
filter = 'all';
rerenderAll();
});
document.getElementById('show-active').addEventListener('click', () => {
filter = 'active';
rerenderAll();
});
document.getElementById('show-completed').addEventListener('click', () => {
filter = 'completed';
rerenderAll();
});
// Re-render all todos
function rerenderAll() {
todoList.innerHTML = '';
todos.map().once((todo, id) => {
renderTodo(todo, id);
});
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Make functions global for inline handlers
window.toggleTodo = toggleTodo;
window.deleteTodo = deleteTodo;
</script>
</body>
</html>
CRUD Operations Explained
Create
// Add to a set (generates random ID)
todos.set('Buy milk');
// Add with specific key
todos.get('todo-1').put('Buy milk');
// Add object
todos.set({
text: 'Buy milk',
completed: false,
createdAt: Date.now()
});
Read
// Read once
todos.once((data) => {
console.log(data);
});
// Listen for changes (real-time)
todos.on((data) => {
console.log('Updated:', data);
});
// Read all items in set
todos.map().on((item, id) => {
console.log(id, item);
});
Update
// Update entire object
todos.get('todo-1').put({
text: 'Buy milk',
completed: true
});
// Update specific property
todos.get('todo-1').get('completed').put(true);
// Partial update (merge)
todos.get('todo-1').get('completed').put(true);
todos.get('todo-1').get('text').put('Buy organic milk');
Delete
// Delete by setting to null
todos.get('todo-1').put(null);
// Delete property
todos.get('todo-1').get('completed').put(null);
// Handle deletion in UI
todos.map().on((todo, id) => {
if (todo === null) {
document.getElementById(id)?.remove();
}
});
Data Patterns
Using Sets vs. Objects
// Set: Good for lists (auto-generates IDs)
const todoSet = gun.get('todos');
todoSet.set('Item 1'); // Creates unique ID
todoSet.set('Item 2');
// Object: Good for named properties
const todoObj = gun.get('todo-1');
todoObj.put({
text: 'Item 1',
completed: false
});
Timestamps and Sorting
// Add timestamp
const todo = {
text: 'Buy milk',
createdAt: Gun.state() // Vector clock
};
// Or JavaScript timestamp
const todo = {
text: 'Buy milk',
createdAt: Date.now()
};
// Sort by timestamp
const sortedTodos = Object.entries(todos)
.sort(([, a], [, b]) => a.createdAt - b.createdAt);
Relations
// Link todos to users
const user = gun.user();
const todos = user.get('todos');
// Add todo
todos.set({
text: 'Buy milk',
completed: false
});
// Reference in multiple places
const todo = gun.get('todo-1').put({ text: 'Buy milk' });
user.get('todos').set(todo);
gun.get('all-todos').set(todo);
Best Practices
- Always validate input before adding to GUN
- Escape HTML to prevent XSS attacks
- Use timestamps for sorting and conflict resolution
- Handle null values for deletions
- Use
.once()for initial load,.on()for updates - Clean up listeners to prevent memory leaks
Next Steps
Chat App
Build a real-time chat application
Collaborative Editor
Build a collaborative text editor
User Authentication
Add user accounts to your app
API Reference
Explore the full API