Skip to main content
Version: 0.13.0

Actions

We'll explain what Actions are and how to use them. If you're looking for a detailed API specification, skip ahead to the API Reference.

Actions are quite similar to Queries, but with a key distinction: Actions are designed to modify and add data, while Queries are solely for reading data. Examples of Actions include adding a comment to a blog post, liking a video, or updating a product's price.

Actions and Queries work together to keep data caches up-to-date.

tip

Actions are almost identical to Queries in terms of their API. Therefore, if you're already familiar with Queries, you might find reading the entire guide repetitive.

We instead recommend skipping ahead and only reading the differences between Queries and Actions, and consulting the API Reference as needed.

Working with Actions

Actions are declared in Wasp and implemented in NodeJS. Wasp runs Actions within the server's context, but it also generates code that allows you to call them from anywhere in your code (either client or server) using the same interface.

This means you don't have to worry about building an HTTP API for the Action, managing server-side request handling, or even dealing with client-side response handling and caching. Instead, just focus on developing the business logic inside your Action, and let Wasp handle the rest!

To create an Action, you need to:

  1. Declare the Action in Wasp using the action declaration.
  2. Implement the Action's NodeJS functionality.

Once these two steps are completed, you can use the Action from anywhere in your code.

Declaring Actions

To create an Action in Wasp, we begin with an action declaration. Let's declare two Actions - one for creating a task, and another for marking tasks as done:

main.wasp
// ...

action createTask {
fn: import { createTask } from "@src/actions.js"
}

action markTaskAsDone {
fn: import { markTaskAsDone } from "@src/actions.js"
}

If you want to know about all supported options for the action declaration, take a look at the API Reference.

The names of Wasp Actions and their implementations don't necessarily have to match. However, to avoid confusion, we'll keep them the same.

tip

Wasp uses superjson under the hood. This means you're not limited to only sending and receiving JSON payloads.

You can send and receive any superjson-compatible payload (like Dates, Sets, Lists, circular references, etc.) and let Wasp handle the (de)serialization.

After declaring a Wasp Action, two important things happen:

  • Wasp generates a server-side NodeJS function that shares its name with the Action.

  • Wasp generates a client-side JavaScript function that shares its name with the Action (e.g., markTaskAsDone). This function takes a single optional argument - an object containing any serializable data you wish to use inside the Action. Wasp will send this object over the network and pass it into the Action's implementation as its first positional argument (more on this when we look at the implementations). Such an abstraction works thanks to an HTTP API route handler Wasp generates on the server, which calls the Action's NodeJS implementation under the hood.

Generating these two functions ensures a uniform calling interface across the entire app (both client and server).

Implementing Actions in Node

Now that we've declared the Action, what remains is to implement it. We've instructed Wasp to look for the Actions' implementations in the file src/actions.ts, so that's where we should export them from.

Here's how you might implement the previously declared Actions createTask and markTaskAsDone:

src/actions.js
// our "database"
let nextId = 4
const tasks = [
{ id: 1, description: 'Buy some eggs', isDone: true },
{ id: 2, description: 'Make an omelette', isDone: false },
{ id: 3, description: 'Eat breakfast', isDone: false },
]

// You don't need to use the arguments if you don't need them
export const createTask = (args) => {
const newTask = {
id: nextId,
isDone: false,
description: args.description,
}
nextId += 1
tasks.push(newTask)
return newTask
}

// The 'args' object is something sent by the caller (most often from the client)
export const markTaskAsDone = (args) => {
const task = tasks.find((task) => task.id === args.id)
if (!task) {
// We'll show how to properly handle such errors later
return
}
task.isDone = true
}
tip

Wasp uses superjson under the hood. This means you're not limited to only sending and receiving JSON payloads.

You can send and receive any superjson-compatible payload (like Dates, Sets, Lists, circular references, etc.) and let Wasp handle the (de)serialization.

For a detailed explanation of the Action definition API (i.e., arguments and return values), check the API Reference.

Using Actions

To use an Action, you can import it from wasp/client/operations and call it directly. As mentioned, the usage doesn't change depending on whether you're on the server or the client:

import { createTask, markTasAsDone } from 'wasp/client/operations'

// ...

const newTask = await createTask({ description: 'Learn TypeScript' })
await markTasAsDone({ id: 1 })

When using Actions on the client, you'll most likely want to use them inside a component:

src/pages/Task.jsx
import React from 'react'
import { useQuery, getTask, markTaskAsDone } from 'wasp/client/operations'

export const TaskPage = ({ id }) => {
const { data: task } = useQuery(getTask, { id })

if (!task) {
return <h1>"Loading"</h1>
}

const { description, isDone } = task
return (
<div>
<p>
<strong>Description: </strong>
{description}
</p>
<p>
<strong>Is done: </strong>
{isDone ? 'Yes' : 'No'}
</p>
{isDone || (
<button onClick={() => markTaskAsDone({ id })}>Mark as done.</button>
)}
</div>
)
}

Since Actions don't require reactivity, they are safe to use inside components without a hook. Still, Wasp provides comes with the useAction hook you can use to enhance actions. Read all about it in the API Reference.

Error Handling

For security reasons, all exceptions thrown in the Action's NodeJS implementation are sent to the client as responses with the HTTP status code 500, with all other details removed. Hiding error details by default helps against accidentally leaking possibly sensitive information over the network.

If you do want to pass additional error information to the client, you can construct and throw an appropriate HttpError in your implementation:

src/actions.js
import { HttpError } from 'wasp/server'

export const createTask = async (args, context) => {
throw new HttpError(
403, // status code
"You can't do this!", // message
{ foo: 'bar' } // data
)
}

Using Entities in Actions

In most cases, resources used in Actions will be Entities. To use an Entity in your Action, add it to the action declaration in Wasp:

main.wasp

action createTask {
fn: import { createTask } from "@src/actions.js",
entities: [Task]
}

action markTaskAsDone {
fn: import { markTaskAsDone } from "@src/actions.js",
entities: [Task]
}

Wasp will inject the specified Entity into the Action's context argument, giving you access to the Entity's Prisma API. Wasp invalidates frontend Query caches by looking at the Entities used by each Action/Query. Read more about Wasp's smart cache invalidation here.

src/actions.js
// The 'args' object is the payload sent by the caller (most often from the client)
export const createTask = async (args, context) => {
const newTask = await context.entities.Task.create({
data: {
description: args.description,
isDone: false,
},
})
return newTask
}

export const markTaskAsDone = async (args, context) => {
await context.entities.Task.update({
where: { id: args.id },
data: { isDone: true },
})
}

The object context.entities.Task exposes prisma.task from Prisma's CRUD API.

Cache Invalidation

One of the trickiest parts of managing a web app's state is making sure the data returned by the Queries is up to date. Since Wasp uses react-query for Query management, we must make sure to invalidate Queries (more specifically, their cached results managed by react-query) whenever they become stale.

It's possible to invalidate the caches manually through several mechanisms react-query provides (e.g., refetch, direct invalidation). However, since manual cache invalidation quickly becomes complex and error-prone, Wasp offers a faster and a more effective solution to get you started: automatic Entity-based Query cache invalidation. Because Actions can (and most often do) modify the state while Queries read it, Wasp invalidates a Query's cache whenever an Action that uses the same Entity is executed.

For example, if the Action createTask and Query getTasks both use the Entity Task, executing createTask may cause the cached result of getTasks to become outdated. In response, Wasp will invalidate it, causing getTasks to refetch data from the server and update it.

In practice, this means that Wasp keeps the Queries "fresh" without requiring you to think about cache invalidation.

On the other hand, this kind of automatic cache invalidation can become wasteful (some updates might not be necessary) and will only work for Entities. If that's an issue, you can use the mechanisms provided by react-query for now, and expect more direct support in Wasp for handling those use cases in a nice, elegant way.

If you wish to optimistically set cache values after performing an Action, you can do so using optimistic updates. Configure them using Wasp's useAction hook. This is currently the only manual cache invalidation mechanism Wasps supports natively. For everything else, you can always rely on react-query.

Differences Between Queries and Actions

Actions and Queries are two closely related concepts in Wasp. They might seem to perform similar tasks, but Wasp treats them differently, and each concept represents a different thing.

Here are the key differences between Queries and Actions:

  1. Actions can (and often should) modify the server's state, while Queries are only permitted to read it. Wasp relies on you adhering to this convention when performing cache invalidations, so it's crucial to follow it.
  2. Actions don't need to be reactive, so you can call them directly. However, Wasp does provide a useAction React hook for adding extra behavior to the Action (like optimistic updates).
  3. action declarations in Wasp are mostly identical to query declarations. The only difference lies in the declaration's name.

API Reference

Declaring Actions in Wasp

The action declaration supports the following fields:

  • fn: ExtImport required

    The import statement of the Action's NodeJs implementation.

  • entities: [Entity]

    A list of entities you wish to use inside your Action. For instructions on using Entities in Actions, take a look at the guide.

Example

Declaring the Action:

query createFoo {
fn: import { createFoo } from "@src/actions.js"
entities: [Foo]
}

Enables you to import and use it anywhere in your code (on the server or the client):

import { createFoo } from 'wasp/client/operations'

Implementing Actions

The Action's implementation is a NodeJS function that takes two arguments (it can be an async function if you need to use the await keyword). Since both arguments are positional, you can name the parameters however you want, but we'll stick with args and context:

  1. args (type depends on the Action)

    An object containing the data passed in when calling the Action (e.g., filtering conditions). Check the usage examples to see how to pass this object to the Action.

  2. context (type depends on the Action)

    An additional context object passed into the Action by Wasp. This object contains user session information, as well as information about entities. Check the section about using entities in Actions to see how to use the entities field on the context object, or the auth section to see how to use the user object.

Example

The following Action:

action createFoo {
fn: import { createFoo } from "@src/actions.js"
entities: [Foo]
}

Expects to find a named export createfoo from the file src/actions.js

actions.js
export const createFoo = (args, context) => {
// implementation
}

The useAction Hook and Optimistic Updates

Make sure you understand how Queries and Cache Invalidation work before reading this chapter.

When using Actions in components, you can enhance them with the help of the useAction hook. This hook comes bundled with Wasp, and is used for decorating Wasp Actions. In other words, the hook returns a function whose API matches the original Action while also doing something extra under the hood (depending on how you configure it).

The useAction hook accepts two arguments:

  • actionFn required

    The Wasp Action (i.e., the client-side Action function generated by Wasp based on a Action declaration) you wish to enhance.

  • actionOptions

    An object configuring the extra features you want to add to the given Action. While this argument is technically optional, there is no point in using the useAction hook without providing it (it would be the same as using the Action directly). The Action options object supports the following fields:

    • optimisticUpdates

      An array of objects where each object defines an optimistic update to perform on the Query cache. To define an optimistic update, you must specify the following properties:

      • getQuerySpecifier required

      A function returning the Query specifier (i.e., a value used to address the Query you want to update). A Query specifier is an array specifying the query function and arguments. For example, to optimistically update the Query used with useQuery(fetchFilteredTasks, {isDone: true }], your getQuerySpecifier function would have to return the array [fetchFilteredTasks, { isDone: true}]. Wasp will forward the argument you pass into the decorated Action to this function (i.e., you can use the properties of the added/changed item to address the Query).

      • updateQuery required

      The function used to perform the optimistic update. It should return the desired state of the cache. Wasp will call it with the following arguments:

      • item - The argument you pass into the decorated Action.
      • oldData - The currently cached value for the Query identified by the specifier.
caution

The updateQuery function must be a pure function. It must return the desired cache value identified by the getQuerySpecifier function and must not perform any side effects.

Also, make sure you only update the Query caches affected by your Action causing the optimistic update (Wasp cannot yet verify this).

Finally, your implementation of the updateQuery function should work correctly regardless of the state of oldData (e.g., don't rely on array positioning). If you need to do something else during your optimistic update, you can directly use react-query's lower-level API (read more about it here).

Here's an example showing how to configure the Action markTaskAsDone that toggles a task's isDone status to perform an optimistic update:

src/pages/Task.jsx
import React from 'react'
import {
useQuery,
useAction,
getTask,
markTaskAsDone,
} from 'wasp/client/operations'

const TaskPage = ({ id }) => {
const { data: task } = useQuery(getTask, { id })
const markTaskAsDoneOptimistically = useAction(markTaskAsDone, {
optimisticUpdates: [
{
getQuerySpecifier: ({ id }) => [getTask, { id }],
updateQuery: (_payload, oldData) => ({ ...oldData, isDone: true }),
},
],
})

if (!task) {
return <h1>"Loading"</h1>
}

const { description, isDone } = task
return (
<div>
<p>
<strong>Description: </strong>
{description}
</p>
<p>
<strong>Is done: </strong>
{isDone ? 'Yes' : 'No'}
</p>
{isDone || (
<button onClick={() => markTaskAsDoneOptimistically({ id })}>
Mark as done.
</button>
)}
</div>
)
}

export default TaskPage

Advanced usage

The useAction hook currently only supports specifying optimistic updates. You can expect more features in future versions of Wasp.

Wasp's optimistic update API is deliberately small and focuses exclusively on updating Query caches (as that's the most common use case). You might need an API that offers more options or a higher level of control. If that's the case, instead of using Wasp's useAction hook, you can use react-query's useMutation hook and directly work with their low-level API.

If you decide to use react-query's API directly, you will need access to Query cache key. Wasp internally uses this key but abstracts it from the programmer. Still, you can easily obtain it by accessing the queryCacheKey property on any Query:

import { getTasks } from 'wasp/client/operations'

const queryKey = getTasks.queryCacheKey