Todo List
This example show how to use lists in combination with forms.
The source code of the example can be found here.
What's an example section without a todo list?! Let's put one together in Raycast. This example will show how to render a list, navigate to a form to create a new element and update the list.
Example: A simple todo list

Render todo list

Let's start with a set of todos and simply render them as a list in Raycast:
1
import { List } from "@raycast/api";
2
import { useState } from "react";
3
4
interface Todo {
5
title: string;
6
isCompleted: boolean;
7
}
8
9
export default function Command() {
10
const [todos, setTodos] = useState<Todo[]>([
11
{ title: "Write a todo list extension", isCompleted: false },
12
{ title: "Explain it to others", isCompleted: false },
13
]);
14
15
return (
16
<List>
17
{todos.map((todo) => (
18
<List.Item title={todo.title} />
19
))}
20
</List>
21
);
22
}
Copied!
For this we define a TypeScript interface to describe out Todo with a title and a isCompleted flag that we use later to complete the todo. We use React's useState hook to create a local state of our todos. This allows us to update them later and the list will get re-rendered. Lastly we render a list of all todos.

Create a todo

A static list of todos isn't that much fun. Let's create new ones with a form. For this, we create a new React component that renders the form:
1
function CreateTodoForm(props: { onCreate: (todo: Todo) => void }) {
2
const { pop } = useNavigation();
3
4
function handleSubmit(values: { title: string }) {
5
props.onCreate({ title: values.title, isCompleted: false });
6
pop();
7
}
8
9
return (
10
<Form
11
actions={
12
<ActionPanel>
13
<SubmitFormAction title="Create Todo" onSubmit={handleSubmit} />
14
</ActionPanel>
15
}
16
>
17
<Form.TextField id="title" title="Title" />
18
</Form>
19
);
20
}
21
Copied!
The <CreateTodoForm> shows a single text field for the title. When the form is submitted, it calls the onCreate callback and closes itself.
Create todo form
To use the action, we add it to the <List> component. This makes the action available when the list is empty which is exactly what we want to create our first todo.
1
export default function Command() {
2
const [todos, setTodos] = useState<Todo[]>([]);
3
4
function handleCreate(todo: Todo) {
5
const newTodos = [...todos, todo];
6
setTodos(newTodos);
7
}
8
9
return (
10
<List
11
actions={
12
<ActionPanel>
13
<CreateTodoAction onCreate={handleCreate} />
14
</ActionPanel>
15
}
16
>
17
{todos.map((todo, index) => (
18
<List.Item
19
key={index}
20
title={todo.title}
21
/>
22
))}
23
</List>
24
);
25
}
Copied!

Complete a todo

Now that we can create new todos, we also want to make sure that we can tick off something on our todo list. For this, we create a <ToggleTodoAction> that we assign to the <List.Item>:
1
export default function Command() {
2
const [todos, setTodos] = useState<Todo[]>([]);
3
4
// ...
5
6
function handleToggle(index: number) {
7
const newTodos = [...todos];
8
newTodos[index].isCompleted = !newTodos[index].isCompleted;
9
setTodos(newTodos);
10
}
11
12
return (
13
<List
14
actions={
15
<ActionPanel>
16
<CreateTodoAction onCreate={handleCreate} />
17
</ActionPanel>
18
}
19
>
20
{todos.map((todo, index) => (
21
<List.Item
22
key={index}
23
icon={todo.isCompleted ? Icon.Checkmark : Icon.Circle}
24
title={todo.title}
25
actions={
26
<ActionPanel>
27
<ActionPanel.Section>
28
<ToggleTodoAction todo={todo} onToggle={() => handleToggle(index)} />
29
</ActionPanel.Section>
30
</ActionPanel>
31
}
32
/>
33
))}
34
</List>
35
);
36
}
37
38
function ToggleTodoAction(props: { todo: Todo; onToggle: () => void }) {
39
return (
40
<ActionPanel.Item
41
icon={props.todo.isCompleted ? Icon.Circle : Icon.Checkmark}
42
title={props.todo.isCompleted ? "Uncomplete Todo" : "Complete Todo"}
43
onAction={props.onToggle}
44
/>
45
);
46
}
47
Copied!
In this case we added the <ToggleTodoAction> to the list item. By doing this we can use the index to toggle the appropriate todo. We also added an icon to our todo that reflects the isCompleted state.

Delete a todo

Similar to toggling a todo, we also add the possibility to delete one. You can follow the same steps and create a new <DeleteTodoAction> and add it to the <List.Item>.
1
export default function Command() {
2
const [todos, setTodos] = useState<Todo[]>([]);
3
4
// ...
5
6
function handleDelete(index: number) {
7
const newTodos = [...todos];
8
newTodos.splice(index, 1);
9
setTodos(newTodos);
10
}
11
12
return (
13
<List
14
actions={
15
<ActionPanel>
16
<CreateTodoAction onCreate={handleCreate} />
17
</ActionPanel>
18
}
19
>
20
{todos.map((todo, index) => (
21
<List.Item
22
key={index}
23
icon={todo.isCompleted ? Icon.Checkmark : Icon.Circle}
24
title={todo.title}
25
actions={
26
<ActionPanel>
27
<ActionPanel.Section>
28
<ToggleTodoAction todo={todo} onToggle={() => handleToggle(index)} />
29
</ActionPanel.Section>
30
<ActionPanel.Section>
31
<CreateTodoAction onCreate={handleCreate} />
32
<DeleteTodoAction onDelete={() => handleDelete(index)} />
33
</ActionPanel.Section>
34
</ActionPanel>
35
}
36
/>
37
))}
38
</List>
39
);
40
}
41
42
// ...
43
44
function DeleteTodoAction(props: { onDelete: () => void }) {
45
return (
46
<ActionPanel.Item
47
icon={Icon.Trash}
48
title="Delete Todo"
49
shortcut={{ modifiers: ["ctrl"], key: "x" }}
50
onAction={props.onDelete}
51
/>
52
);
53
}
Copied!
We also gave the <DeleteTodoAction> a keyboart shortcut. This way users can delete todos quicker. Additionally, we also added the <CreateTodoAction> to the <List.Item>. This makes sure that users can also create new todos when there are some already.
And that's a wrap. You created a todo list in Raycast, it's that easy. As next steps, you could extract the <CreateTodoForm> into a separate command. Than you can create todos also from the root search of Raycast and can even assign a global hotkey to open the form. Also, the todos aren't persisted. If you close the command and reopen it, they are gone. To persiste, you can use the storage or write it to disc.
Last modified 8d ago