6. Modifying Data
In the previous section, we learned about using queries to fetch data and only briefly mentioned that actions can be used to update the database. Let's learn more about actions so we can add and update tasks in the database.
We have to create:
- A Wasp action that creates a new task.
- A React form that calls that action when the user creates a task.
Creating a New Action
Creating an action is very similar to creating a query.
Declaring an Action
We must first declare the action in main.wasp
:
- JavaScript
- TypeScript
// ...
action createTask {
fn: import { createTask } from "@server/actions.js",
entities: [Task]
}
// ...
action createTask {
fn: import { createTask } from "@server/actions.js",
entities: [Task]
}
Implementing an Action
Let's now define a function for our createTask
action:
- JavaScript
- TypeScript
export const createTask = async (args, context) => {
return context.entities.Task.create({
data: { description: args.description },
})
}
import { Task } from '@wasp/entities'
import { CreateTask } from '@wasp/actions/types'
type CreateTaskPayload = Pick<Task, 'description'>
export const createTask: CreateTask<CreateTaskPayload, Task> = async (
args,
context
) => {
return context.entities.Task.create({
data: { description: args.description },
})
}
Once again, we've annotated the action with the generated CreateTask
and Task
types generated by Wasp. Just like with queries, defining the types on the implemention makes them available on the frontend, giving us full-stack type safety.
We put the function in a new file src/server/actions.ts
, but we could have put it anywhere we wanted! There are no limitations here, as long as the declaration in the Wasp file imports it correctly and the file is located within src/server
.
Invoking the Action on the Client
First, let's define a form that the user can create new tasks with.
- JavaScript
- TypeScript
import getTasks from '@wasp/queries/getTasks'
import createTask from '@wasp/actions/createTask'
import { useQuery } from '@wasp/queries'
// ...
const NewTaskForm = () => {
const handleSubmit = async (event) => {
event.preventDefault()
try {
const target = event.target
const description = target.description.value
target.reset()
await createTask({ description })
} catch (err) {
window.alert('Error: ' + err.message)
}
}
return (
<form onSubmit={handleSubmit}>
<input name="description" type="text" defaultValue="" />
<input type="submit" value="Create task" />
</form>
)
}
import { FormEvent } from 'react'
import getTasks from '@wasp/queries/getTasks'
import createTask from '@wasp/actions/createTask'
import { useQuery } from '@wasp/queries'
import { Task } from '@wasp/entities'
// ...
const NewTaskForm = () => {
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
try {
const target = event.target as HTMLFormElement
const description = target.description.value
target.reset()
await createTask({ description })
} catch (err: any) {
window.alert('Error: ' + err.message)
}
}
return (
<form onSubmit={handleSubmit}>
<input name="description" type="text" defaultValue="" />
<input type="submit" value="Create task" />
</form>
)
}
Unlike queries, you call actions directly (i.e., without wrapping it with a hook) because we don't need reactivity. The rest is just regular React code.
Now, we just need to add this form to the page component:
- JavaScript
- TypeScript
import getTasks from '@wasp/queries/getTasks'
import createTask from '@wasp/actions/createTask'
import { useQuery } from '@wasp/queries'
const MainPage = () => {
const { data: tasks, isLoading, error } = useQuery(getTasks)
return (
<div>
<NewTaskForm />
{tasks && <TasksList tasks={tasks} />}
{isLoading && 'Loading...'}
{error && 'Error: ' + error}
</div>
)
}
import { FormEvent } from 'react'
import getTasks from '@wasp/queries/getTasks'
import createTask from '@wasp/actions/createTask'
import { useQuery } from '@wasp/queries'
import { Task } from '@wasp/entities'
const MainPage = () => {
const { data: tasks, isLoading, error } = useQuery(getTasks)
return (
<div>
<NewTaskForm />
{tasks && <TasksList tasks={tasks} />}
{isLoading && 'Loading...'}
{error && 'Error: ' + error}
</div>
)
}
And now we have a form that creates new tasks.
Try creating a "Build a Todo App in Wasp" task and see it appear in the list below. The task is created on the server and saved in the database.
Try refreshing the page or opening it in another browser, you'll see the tasks are still there!
When you create a new task, the list of tasks is automatically updated to display the new task, even though we have not written any code that would do that! These automatic updates are handled by code that Wasp generates.
When you declared the getTasks
and createTask
operations, you specified that they both use the Task
entity. So when createTask
is called, Wasp knows that the data getTasks
fetches may have changed and automatically updates it in the background. This means that out of the box, Wasp will make sure that all your queries are kept in-sync with changes made by any actions.
This behavior is convenient as a default but can cause poor performance in large apps. While there is no mechanism for overriding this behavior yet, it is something that we plan to include in Wasp in the future. This feature is tracked here.
A Second Action
Our Todo app isn't finished if you can't mark a task as done! We'll create a new action to update a task's status and call it from React whenever a task's checkbox is toggled.
Since we've already created one task together, try to create this one yourself. It should be an action named updateTask
that takes a task id
and an isDone
in its arguments. You can check our implementation below.
Solution
The action declaration:
- JavaScript
- TypeScript
// ...
action updateTask {
fn: import { updateTask } from "@server/actions.js",
entities: [Task]
}
// ...
action updateTask {
fn: import { updateTask } from "@server/actions.js",
entities: [Task]
}
The implementation on the server:
- JavaScript
- TypeScript
// ...
export const updateTask = async ({ id, isDone }, context) => {
return context.entities.Task.update({
where: { id },
data: {
isDone: isDone,
},
})
}
import { CreateTask, UpdateTask } from '@wasp/actions/types'
// ...
type UpdateTaskPayload = Pick<Task, 'id' | 'isDone'>
export const updateTask: UpdateTask<UpdateTaskPayload, Task> = async (
{ id, isDone },
context
) => {
return context.entities.Task.update({
where: { id },
data: {
isDone: isDone,
},
})
}
Now, we can call updateTask
from the React component:
- JavaScript
- TypeScript
// ...
import updateTask from '@wasp/actions/updateTask'
// ...
const Task = ({ task }) => {
const handleIsDoneChange = async (event) => {
try {
await updateTask({
id: task.id,
isDone: event.target.checked,
})
} catch (error) {
window.alert('Error while updating task: ' + error.message)
}
}
return (
<div>
<input
type="checkbox"
id={String(task.id)}
checked={task.isDone}
onChange={handleIsDoneChange}
/>
{task.description}
</div>
)
}
// ...
// ...
import { FormEvent, ChangeEvent } from 'react'
// ...
import updateTask from '@wasp/actions/updateTask'
// ...
const Task = ({ task }: { task: Task }) => {
const handleIsDoneChange = async (event: ChangeEvent<HTMLInputElement>) => {
try {
await updateTask({
id: task.id,
isDone: event.target.checked,
})
} catch (error: any) {
window.alert('Error while updating task: ' + error.message)
}
}
return (
<div>
<input
type="checkbox"
id={String(task.id)}
checked={task.isDone}
onChange={handleIsDoneChange}
/>
{task.description}
</div>
)
}
// ...
Awesome! Now we can check off this task 🙃 Let's add one more interesting feature to our app.