wasp/web/docs/tutorial/06-actions.md

464 lines
11 KiB
Markdown
Raw Normal View History

---
title: 6. Modifying Data
---
import useBaseUrl from '@docusaurus/useBaseUrl';
import { ShowForTs, ShowForJs } from '@site/src/components/TsJsHelpers';
import Collapse from '@site/src/components/Collapse';
2024-07-17 12:54:36 +03:00
In the previous section, you learned about using Queries to fetch data.
Let's now learn about Actions so you can add and update tasks in the database.
2024-07-17 12:54:36 +03:00
In this section, you will create:
2024-02-26 16:33:28 +03:00
1. A Wasp Action that creates a new task.
2. A React form that calls that Action when the user creates a task.
## Creating a New Action
2024-02-26 16:33:28 +03:00
Creating an Action is very similar to creating a Query.
### Declaring an Action
2024-02-26 16:33:28 +03:00
We must first declare the Action in `main.wasp`:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```wasp title="main.wasp"
// ...
action createTask {
2024-02-26 16:33:28 +03:00
fn: import { createTask } from "@src/actions",
entities: [Task]
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```wasp title="main.wasp"
// ...
action createTask {
2024-02-26 16:33:28 +03:00
fn: import { createTask } from "@src/actions",
entities: [Task]
}
```
</TabItem>
</Tabs>
### Implementing an Action
2024-02-26 16:33:28 +03:00
Let's now define a <ShowForJs>JavaScript</ShowForJs><ShowForTs>TypeScript</ShowForTs> function for our `createTask` Action:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
2024-02-26 16:33:28 +03:00
```js title="src/actions.js"
export const createTask = async (args, context) => {
return context.entities.Task.create({
data: { description: args.description },
})
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
2024-02-26 16:33:28 +03:00
```ts title="src/actions.ts"
import { Task } from 'wasp/entities'
import { CreateTask } from 'wasp/server/operations'
type CreateTaskPayload = Pick<Task, 'description'>
export const createTask: CreateTask<CreateTaskPayload, Task> = async (
args,
context
) => {
return context.entities.Task.create({
data: { description: args.description },
})
}
```
2024-02-26 16:33:28 +03:00
Once again, we've annotated the Action with the `CreateTask` and `Task` types generated by Wasp. Just like with queries, defining the types on the implementation makes them available on the frontend, giving us **full-stack type safety**.
</TabItem>
</Tabs>
:::tip
2024-02-26 16:33:28 +03:00
We put the function in a new file `src/actions.{js,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` directory.
:::
## Invoking the Action on the Client
2024-02-26 16:33:28 +03:00
Start by defining a form for creating new tasks.
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
2024-02-26 16:33:28 +03:00
```jsx title="src/MainPage.jsx"
import {
// highlight-next-line
createTask,
getTasks,
useQuery
} from 'wasp/client/operations'
2024-02-26 16:33:28 +03:00
// ... MainPage, TaskView, TaskList ...
2024-02-26 16:33:28 +03:00
// highlight-start
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>
)
}
2024-02-26 16:33:28 +03:00
// highlight-end
```
</TabItem>
<TabItem value="ts" label="TypeScript">
2024-02-26 16:33:28 +03:00
```tsx title="src/MainPage.tsx"
// highlight-next-line
import { FormEvent } from 'react'
2024-02-26 16:33:28 +03:00
import { Task } from 'wasp/entities'
import {
// highlight-next-line
createTask,
getTasks,
useQuery
} from 'wasp/client/operations'
2024-02-26 16:33:28 +03:00
// ... MainPage, TaskView, TaskList ...
2024-02-26 16:33:28 +03:00
// highlight-start
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>
)
}
2024-02-26 16:33:28 +03:00
// highlight-end
```
</TabItem>
</Tabs>
2024-07-16 20:51:51 +03:00
Unlike Queries, you can call Actions directly (without wrapping them in a hook) because they don't need reactivity. The rest is just regular React code.
<ShowForTs>
2024-02-26 16:33:28 +03:00
Finally, because we've previously annotated the Action's server implementation with the correct type, Wasp knows that the `createTask` Action expects a value of type `{ description: string }` (try changing the argument and reading the error message). Wasp also knows that a call to the `createTask` Action returns a `Task` but are not using it in this example.
</ShowForTs>
2024-02-26 16:33:28 +03:00
All that's left now is adding this form to the page component:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
2024-02-26 16:33:28 +03:00
```jsx title="src/MainPage.jsx"
import {
createTask,
getTasks,
useQuery
} from 'wasp/client/operations'
const MainPage = () => {
const { data: tasks, isLoading, error } = useQuery(getTasks)
return (
<div>
2024-02-26 16:33:28 +03:00
// highlight-next-line
<NewTaskForm />
{tasks && <TasksList tasks={tasks} />}
{isLoading && 'Loading...'}
{error && 'Error: ' + error}
</div>
)
}
2024-02-26 16:33:28 +03:00
// ... TaskView, TaskList, NewTaskForm ...
```
</TabItem>
<TabItem value="ts" label="TypeScript">
2024-02-26 16:33:28 +03:00
```tsx title="src/MainPage.tsx"
import { FormEvent } from 'react'
2024-02-26 16:33:28 +03:00
import { Task } from 'wasp/entities'
import {
createTask,
getTasks,
useQuery
} from 'wasp/client/operations'
const MainPage = () => {
const { data: tasks, isLoading, error } = useQuery(getTasks)
return (
<div>
2024-02-26 16:33:28 +03:00
// highlight-next-line
<NewTaskForm />
{tasks && <TasksList tasks={tasks} />}
{isLoading && 'Loading...'}
{error && 'Error: ' + error}
</div>
)
}
2024-02-26 16:33:28 +03:00
// ... TaskList, TaskView, NewTaskForm ...
```
</TabItem>
</Tabs>
2024-02-26 16:33:28 +03:00
Great work!
You now have a form for creating 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.
2024-02-26 16:33:28 +03:00
Try refreshing the page or opening it in another browser. You'll see the tasks are still there!
<img alt="Todo App - creating new task"
src={useBaseUrl('img/todo-app-new-task.png')}
style={{ border: "1px solid black" }}
/>
<br />
<br />
:::note Automatic Query Invalidation
2024-07-16 20:51:51 +03:00
When you create a new task, the list of tasks is automatically updated to display the new task, even though you haven't written any code that does that! Wasp handles these automatic updates under the hood.
2024-02-26 16:33:28 +03:00
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 keeps all your queries in sync with any changes made through 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](https://github.com/wasp-lang/wasp/issues/63).
:::
## A Second Action
2024-02-26 16:33:28 +03:00
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.
2024-02-26 16:33:28 +03:00
Since we've already created one task together, try to create this one yourself. It should be an Action named `updateTask` that receives the task's `id` and its `isDone` status. You can see our implementation below.
<Collapse title="Solution">
2024-02-26 16:33:28 +03:00
Declaring the Action in `main.wasp`:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
```wasp title="main.wasp"
// ...
action updateTask {
2024-02-26 16:33:28 +03:00
fn: import { updateTask } from "@src/actions",
entities: [Task]
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
```wasp title="main.wasp"
// ...
action updateTask {
2024-02-26 16:33:28 +03:00
fn: import { updateTask } from "@src/actions",
entities: [Task]
}
```
</TabItem>
</Tabs>
2024-02-26 16:33:28 +03:00
Implementing the Action on the server:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
2024-02-26 16:33:28 +03:00
```js title="src/actions.js"
// ...
export const updateTask = async ({ id, isDone }, context) => {
return context.entities.Task.update({
where: { id },
data: {
isDone: isDone,
},
})
}
```
</TabItem>
<TabItem value="ts" label="TypeScript">
2024-02-26 16:33:28 +03:00
```ts title="src/actions.ts"
import { CreateTask, UpdateTask } from 'wasp/server/operations'
// ...
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,
},
})
}
```
</TabItem>
</Tabs>
</Collapse>
2024-02-26 16:33:28 +03:00
You can now call `updateTask` from the React component:
<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">
2024-02-26 16:33:28 +03:00
```jsx title="src/MainPage.jsx"
// ...
2024-02-26 16:33:28 +03:00
import {
// highlight-next-line
updateTask,
createTask,
getTasks,
useQuery,
} from 'wasp/client/operations'
// ... MainPage ...
const TaskView = ({ task }) => {
// highlight-start
const handleIsDoneChange = async (event) => {
try {
await updateTask({
id: task.id,
isDone: event.target.checked,
})
2023-10-16 14:02:09 +03:00
} catch (error) {
window.alert('Error while updating task: ' + error.message)
}
}
2024-02-26 16:33:28 +03:00
// highlight-end
return (
<div>
<input
type="checkbox"
id={String(task.id)}
checked={task.isDone}
2024-02-26 16:33:28 +03:00
// highlight-next-line
onChange={handleIsDoneChange}
/>
{task.description}
</div>
)
}
2024-02-26 16:33:28 +03:00
// ... TaskList, NewTaskForm ...
```
</TabItem>
<TabItem value="ts" label="TypeScript">
2024-02-26 16:33:28 +03:00
```tsx title="src/MainPage.tsx"
// highlight-next-line
import { FormEvent, ChangeEvent } from 'react'
2024-02-26 16:33:28 +03:00
import { Task } from 'wasp/entities'
import {
// highlight-next-line
updateTask,
createTask,
getTasks,
useQuery,
} from 'wasp/client/operations'
2024-02-26 16:33:28 +03:00
// ... MainPage ...
const TaskView = ({ task }: { task: Task }) => {
// highlight-start
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)
}
}
2024-02-26 16:33:28 +03:00
// highlight-end
return (
<div>
<input
type="checkbox"
id={String(task.id)}
checked={task.isDone}
2024-02-26 16:33:28 +03:00
// highlight-next-line
onChange={handleIsDoneChange}
/>
{task.description}
</div>
)
}
2024-02-26 16:33:28 +03:00
// ... TaskList, NewTaskForm ...
```
</TabItem>
</Tabs>
2024-02-26 16:33:28 +03:00
Awesome!
You can now mark this task as done.
It's time to make one final addition to your app: supporting multiple users.