Time Management Web App: CRUD for Tasks

Shawn
6 min readOct 14, 2022

--

In this section we’ll set up the tasks page. This will simply display a table with all the tasks in the database, their cron string, and whether they’ve been marked as completed. The component holding the table will fetch the list of tasks. Each row of the table will be a separate component, and at the bottom of the table we’ll have a place to add a new task.

For those who don’t know, cron expressions are specially-formatted strings used to run code at certain intervals. They’re used on Unix systems. A cron expression like * * * * * runs every minute. The expression 30 17 4 8 * runs every August the 4th at 5:30 PM. On the backend we use a Node module called node-schedule , which uses cron strings to determine when tasks start.

This first iteration of the front end will simply show the cron string itself. Later on we’ll fix it to be more human-readable.

Fetching the Data

Before we can populate the table, we need to get the data. Since we already have the backend API set up, it makes sense to start with the object that handles interacting with the API. Create a file called tasks.js and create the object:

const url = 'http://localhost:3001/api/task';
const headers = {
'Content-Type': 'application/json'
};
const API = {
async getAll() {
const res = await fetch(`${url}/all`, {method: 'GET', headers});
const {result} = await res.json();
return result;
},
async create(body) {
const res = await fetch(url, {method: 'POST', headers, body: JSON.stringify(body)});
const {result} = await res.json();
return result;
},
async update(id, body) {
const res = await fetch(`${url}/${id}`, {method: 'PUT', headers, body: JSON.stringify(body)});
const {result} = await res.json();
return result;
},
async del(id) {
const res = await fetch(`${url}/${id}`, {method: 'DELETE', headers});
const {result} = await res.json();
return result;
}
}

Task Table Skeleton

Now we add the table itself, along with the code that gets the task list.

import {useState, useEffect} from 'react';...const TaskTable = props => {
const [tasks, setTasks] = useState([]);
useEffect(() => {
async function getTasks() {
const data = await API.getAll();
setTasks(data);
}
getTasks();
}, []);
return <table border='1'>
<thead>
<tr>
<th>Completed</th>
<th>Name</th>
<th>Cron String</th>
<th>Edit</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
}
export default TaskTable;

The useEffect may look a little weird, because getTasks is defined as being async yet isn’t called with an await. This is a quirk of how useEffect works. The hook expects either no value returned, or a function that serves as cleanup code. Asynchronous functions always return a Promise . This would cause bugs since it’s not what React expects, so we don’t include the await.

Adding Tasks

Before we can display tasks, we need the ability to add one. Although it might be more “proper” to put the components in their own files, for the sake of simplicity we’ll put the components for the table rows in the same tasks.js file. The NewTask component:

import {isValidCron} from 'cron-validator';...const NewTask = props => {
const [task, setTask] = useState({name: '', cron_string: '', completed: false});
const handleChange = e => setTask({...task, [e.target.name]: e.target.value});
const save = async e => {
const result = await API.create(task);
props.handleSave(result);
setTask({name: '', cron_string: '', completed: false});
}
return <tr>
<td></td>
<td><input type='text' name='name' value={task.name} onChange={handleChange} /></td>
<td><input type='text' name='cron_string' value={task.cron_string} onChange={handleChange} /></td>
<td><button onClick={save} disabled={!task.name || !isValidCron(task.cron_string)}>Save</button></td>
<td></td>
</tr>
}

This component draws a table row with:

  • A text field for the task name
  • A text field for the cron string
  • A save button that is only enabled if the name field is populated and the cron string field is populated with a valid cron string (using the isValidCron function from the cron-validator package)
  • An empty table cell to match what the rest of the table will look like

When you hit Save, this component makes the API call. Then it calls the handleSave function passed to it by the TaskTable component. handleSave simply adds the newly added task to the top-level list:

const handleSave = task => {
setTasks([...tasks, task]);
}

Now we can add the NewTask component to the TaskTable , in the <tbody> tag:

<NewTask handleSave={handleSave} />

In order to see this in our project, we add a route over in index.js . We add it as a child Route to the one pointing to <App />:

...
import TaskTable from './tasks';
...
<Route path='tasks' element={<TaskTable />} />

Now after running npm run start and going to http://localhost:3000/tasks, we should see this:

Which, to be fair, doesn’t look very interesting. But you can at least test that if you provide a name and a valid cron string, the Save button will enable. And the Save button does work, it just won’t show anything.

Displaying The Tasks

The TaskRow component will hold each already-existing task:

const TaskRow = props => {
const [task, setTask] = useState(props.task);

return <tr>
<td><input type='checkbox' name='completed' checked={task.completed} /></td>
<td><input type='text' name='name' value={task.name} /></td>
<td><input type='text' name='cron_string' value={task.cron_string} /></td>
<td><button disabled={!task.name || !isValidCron(task.cron_string)}>Update</button></td>
<td><button>Delete</button></td>
</tr>
}

This first iteration of the component simply takes the task passed to it and does the following:

  • Shows a checkbox indicating if it’s completed or not
  • Shows two text inputs for its name and cron string
  • Presents an update button that is only enabled if the name exists and the cron string is valid
  • Presents a button to delete the task

Add it to the TaskTable component immediately above the NewTask component like so:

{tasks.map(t => <TaskRow key={t.id} task={t} />)}

And now after adding a task, this is what you should see:

Modifying Tasks

But right now everything is static. We need event handlers:

const handleClick = e => setTask({...task, completed: !task.completed});

const handleChange = e => setTask({...task, [e.target.name]: e.target.value});
const update = async e => {
const {id, ...rest} = task;
const result = await API.update(id, rest);
props.handleUpdate(id, result);
}
const del = async e => {
if(window.confirm(`Delete task: ${task.name}?`)) {
const result = await API.del(task.id);
props.handleDelete(task.id);
}
}
...return <tr>
<td><input type='checkbox' name='completed' checked={task.completed} onChange={handleClick} /></td>
<td><input type='text' name='name' value={task.name} onChange={handleChange} /></td>
<td><input type='text' name='cron_string' value={task.cron_string} onChange={handleChange} /></td>
<td><button onClick={update} disabled={!task.name || !isValidCron(task.cron_string)}>Update</button></td>
<td><button onClick={del}>Delete</button></td>
</tr>

The bolded text indicates where the handlers were added to the table row. Looking at the update and del functions, we see that like NewTask, this component does the actual API calls. Then there are functions passed from TaskTable through props. Add them as such:

const handleUpdate = (id, task) => {
const oldTasks = [...tasks];
const find = oldTasks.find(t => t.id == id);
if (find) {
find.completed = task.completed;
find.name = task.name;
find.cron_string = task.cron_string;
}
setTasks(oldTasks);
}
const handleDelete = id => {
const oldTasks = [...tasks];
const index = oldTasks.findIndex(t => t.id == id);
if (index > -1) {
oldTasks.splice(index, 1);
}
setTasks(oldTasks);
}
... {tasks.map(t => <TaskRow key={t.id} task={t} handleUpdate={handleUpdate} handleDelete={handleDelete} />)}

The handleUpdate function simply finds the task in the existing list and updates it in place. handleDelete finds the index of the target task and splices it out of the list altogether.

Now we can add and update tasks:

And delete tasks altogether:

Putting It Together

We now have a page that can do the basic CRUD operations on tasks. In the next section we’ll set up sockets and the backend scheduling for unfinished tasks. Here’s what the code should look like at this point.

Time Management Web App Series

--

--