Skip to content

11

Mutating Data

In the previous chapter, you implemented search and pagination using URL Search Params and Qwik-city APIs. Let's continue working on the Invoices page by adding the ability to create, update, and delete invoices!

In this chapter...

Here are the topics we will cover:

What serverAction$() are and how use them to mutate data

How to work with <Form /> or programmatically.

Best practices for working with zod type validation.

How to redirect in server-side actions using requestEvent.

How to create dynamic route segments with specific IDs.

What are routeAction$()?

routeAction$()is used to define functions called actions that execute exclusively on the server, and only when explicitly called. Actions can have side effects such as writing to a database or sending an email, that cannot happen during client-side rendering. This makes them ideal for handling form submissions, performing operations with side effects, and then returning data back to the client/browser where it can be used to update the UI.

Actions can be declared using routeAction$() or globalAction$() exported from @builder.io/qwik-city.

Using forms with routeAction$()

The best way to call an action is using the <Form/> component exported in @builder.io/qwik-city.

For example:

src/routes/index.tsx

Under the hood, the <Form/> component uses a native HTML <Form/> element, so it will work without JavaScript.

When JS is enabled, the <Form/> component will intercept the form submission and trigger the action in SPA mode. Allowing for a full SPA experience.

It’s time to take a quiz!

Test your knowledge and see what you’ve just learned.

What happens when you use the <Form/> component from @builder.io/qwik-city in a Qwik application, and JavaScript is disabled?

You must choose response !

Let's see how it all works together!

Creating a new invoice

Here are the steps you'll take to create a new invoice:

  1. Create a form to capture the user's input.
  2. Create a routeAction$()
  3. Check the data from the form.
  4. Validate the data.
  5. Prepare and insert the data into the database.
  6. Redirect the user back to invoices page.

1. Create a new route and form

To start, inside src/routes/dashboard/invoices folder, add a new route segment called /create with a index.tsx file:

Invoices folder with a nested create folder, and a index.tsx file inside it

You'll be using this route to create new invoices. Inside your index.tsx file, paste the following code, then spend some time studying it:

src/routes/dashboard/invoices/create/index.tsx

Your page now has a breadcrumb navigation and a <CreateForm> component. To save time, we've already created the <CreateForm> component for you.

Navigate to src/components/ui/invoices/create-form.tsx and you'll see that the form:

  • Has one <select> (dropdown) element with a list of customers.
  • Has one <input> element for the amount with type="number".
  • Has two <input> elements for the status with type="radio".
  • Has one button with type="submit".

For fetch and display the list of customers we will use the fetchCustomers() function import from ~/lib/data".

In the src/lib/data.ts file, paste the following code:

src/lib/data.ts

On http://localhost:5173/dashboard/invoices/create, you should see the following UI:

Create invoices page with breadcrumbs and form

2. Create a routeAction$()

Great, now let's create a routeAction$() to handle the form submission.

In the src/routes/dashboard/invoices/create/index.tsx file, create a new routeAction$() function called useCreateInvoice:

src/routes/dashboard/invoices/create/index.tsx

Now, in your <CreateForm> component import and use the useCreateInvoice action:

src/components/ui/invoices/create-form.tsx

3. Check the data from the form

Back in your src/routes/dashboard/invoices/create/index.tsx and add this log:

src/routes/dashboard/invoices/create/index.tsx

To check everything is connected correctly, go ahead and try out the form. After submitting, you should see the data you just entered into the form logged in your terminal.

Now that your data is being passed correctly, you can start working on the logic to insert the data into the database.

4. Validate the data with Zod

Before sending the form data to your database, you want to ensure it's in the correct format and with the correct types. If you remember from earlier in the course, your invoices table expects data in the following format:

src/lib/definitions.ts

So far, you only have the customer_id, amount, and status from the form.

Type validation and coercion

To handle type validation, you have a few options. While you can manually validate types, using a type validation library can save you time and effort. For your example, we'll use Zod.

Qwik comes with built-in support for Zod, a TypeScript-first schema validation that can be used directly with actions, using the zod$() function.

Actions + Zod allows to create type safe forms, where the data is validated server side before the action is executed.

In your src/routes/dashboard/invoices/create/index.tsx

src/routes/dashboard/invoices/create/index.tsx

When submitting data to a routeAction(), the data is validated against the Zod schema. If the data is invalid, the action will put the validation error in the routeAction.value property.

Please refer to the Zod for more information on how to use Zod schemas.

Now, if you go to http://localhost:5173/dashboard/invoices/create/ and you submit your form with invalid data, the action will not be executed, and you should see an error message in the server's console.

Terminal

Great's, now you have a form that validates the data before inserting it into the database.

5. Insert the data into the database

Now that you have validated the data, you can insert it into the database. In your src/lib folder create a new file called actions.ts.

We use this file for all our actions. In this file, you will create a new action function called createInvoice:

src/lib/actions.ts

This function receives in parameters the data from the routeLoader$().

This data has already been checked in the routeLoader$() with zod$. (If the data is invalid, the action will not be executed).

Storing values in cents

It's usually good practice to store monetary values in cents in your database to eliminate JavaScript floating-point errors and ensure greater accuracy.

Let's convert the amount into cents:

src/lib/actions.ts

Creating new dates

Finally, let's create a new date with the format "YYYY-MM-DD" for the invoice's creation date:

src/lib/actions.ts

Now that you have all the values you need for your database, you can create an SQL query to insert the new invoice into your database and pass in the variables:

src/lib/actions.ts

In this code, you're using the getPool() function from the data.ts file to connect to the database. We need export it from the file.

src/lib/data.ts

Now, all we need to do is import and use our createInvoice() function in our routeAction$()

src/routes/dashboard/invoices/create/index.tsx

Right now, we're not handling any errors. We'll do it in the next chapter. For now, let's move on to the next step.

6. Redirect

After successfully inserting the data into the database, you want to redirect the user back to the invoices page. To do this, you can use the redirect method from the requestEvent object.

src/routes/dashboard/invoices/create/index.tsx

Now, when you submit the form, you should be redirected back to the invoices page.

In the next chapter, you'll learn how to handle errors and display them to the user.

Congratulations! You've just implemented your first Server Action. Test it out by adding a new invoice, if everything is working correctly:

  1. You should be redirected to the /dashboard/invoices route on submission.
  2. You should see the new invoice at the top of the table.

Updating an invoice

The updating invoice form is similar to the create an invoice form, except you'll need to pass the invoice id to update the record in your database. Let's see how you can get and pass the invoice id.

These are the steps you'll take to update an invoice:

  1. Create a new dynamic route segment with the invoice id.
  2. Read the invoice id from the URL params.
  3. Fetch the specific invoice from your database.
  4. Pre-populate the form with the invoice data.
  5. Update the invoice data in your database.

1. Create a Dynamic Route Segment with the invoice id

Qwik allows you to create Dynamic Route Segments when you don't know the exact segment name and want to create routes based on data. This could be blog post titles, product pages, etc. You can create dynamic route segments by wrapping a folder's name in square brackets. For example, [id][post] or [slug].

In your /invoices folder, create a new dynamic route called [id], then a new route called edit with a index.tsx file. Your file structure should look like this:

Invoices folder with a nested [id] folder, and an edit folder inside it

In your <Table> component, you'll need to update the two <UpdateInvoice /> buttons (mobile and desktop) to pass the invoice id as a parameter.

src/components/ui/invoices/table.tsx

Navigate to your <UpdateInvoice /> component, and update the href of the Link to accept the id prop. You can use template literals to link to a dynamic route segment:

src/components/ui/invoices/buttons.tsx

2. Read the invoice id from the URL params

Back on yoursrc/routes/dashboard/invoices/edit/index.tsxfile, paste the following code:

src/routes/dashboard/invoices/[id]/edit/index.tsx

Notice how it's similar to your /create invoice page, except it imports a different form (from the edit-form.tsx file). This form should be pre-populated with a value for the customer's name, invoice amount, and status. To pre-populate the form fields, you need to fetch the specific invoice using id.

Update your component to receive the prop:

src/routes/dashboard/invoices/[id]/edit/index.tsx

3. Fetch the specific invoice

Then:

  • Import a new function called fetchInvoiceById and pass the id as an argument.
  • Import fetchCustomers to fetch the customer names for the dropdown.

You can use Promise.all to fetch both the invoice and customers in parallel:

src/routes/dashboard/invoices/[id]/edit/index.tsx

In your src/lib/data.ts file, create a new function to fetch the invoice by id:

src/lib/data.ts

Great! Now, test that everything is wired correctly. Visit http://localhost:5173/dashboard/invoices and click on the Pencil icon to edit an invoice. After navigation, you should see a form that is pre-populated with the invoice details:

Edit invoices page with breadcrumbs and form

The URL should also be updated with an id as follows: http://localhost:3000/dashboard/invoice/uuid/edit

UUIDs vs. Auto-incrementing Keys

We use UUIDs instead of incrementing keys (e.g., 1, 2, 3, etc.). This makes the URL longer; however, UUIDs eliminate the risk of ID collision, are globally unique, and reduce the risk of enumeration attacks - making them ideal for large databases.

However, if you prefer cleaner URLs, you might prefer to use auto-incrementing keys.

4. Get the id into the routeAction$()

routeAction$ and globalAction$ have access to the RequestEvent object which includes information about the current HTTP request and response.

This allows actions to access the request headers, cookies, url and environment variables within the routeAction$ function.

Now that you have the invoice id, you can pass it to the routeAction$() function to update the invoice in the database.

In your src/routes/dashboard/invoices/[id]/edit/index.tsx file, create a new routeAction$() function called useUpdateInvoice:

You can use the params object to access the id from the URL:

src/routes/dashboard/invoices/[id]/edit/index.tsx

In your form:

src/components/ui/invoices/edit-form.tsx

Now, if you submit the form, you should see the invoice id logged in your terminal.

Note: Using a hidden input field in your form also works (e.g. <input type="hidden" name="id" value={invoice.id} />). However, the values will appear as full text in the HTML source, which is not ideal for sensitive data like IDs.

5. Update the invoice in the database

Similary to the createInvoice action, here you are:

  1. Check the data with Zod before inserting it into the database.
  2. Create a new action called updateInvoice to update the invoice in the database.
  3. Redirect the user back to the invoices page after successfully updating the invoice.

In your src/routes/dashboard/invoices/[id]/edit/index.tsx file:

src/routes/dashboard/invoices/[id]/edit/index.tsx

In your src/lib/actions.ts file, create a new action called updateInvoice:

src/lib/actions.ts

Redirect the user back to the invoices page after updating the invoice:

src/routes/dashboard/invoices/[id]/edit/index.tsx

Test it out by editing an invoice. After submitting the form, you should be redirected to the invoices page, and the invoice should be updated.

Congratulations! You've successfully implemented the update invoice !! 🎉

Delete an invoice

To delete an invoice using routeAction$(), you need pass the invoice id to the action. Here's how you can do it:

As seen previously, we cannot pass props directly to a routeAction$().

When modifying an invoice we were able to retrieve the invoice id from the URL, but deleting an invoice is not done in the invoice details page, but in the invoice list page. So we don't have the invoice id in the URL.

To delete an invoice, with routeAction$(), we will using actions programmatically.

Go to the src/components/ui/invoices/table.tsx file and add an id props to the TWO DeleteInvoice components:

src/components/ui/invoices/table.tsx

Actions can also be triggered programmatically using the action.submit() method (i.e. you don't need a <Form/> component). However, you can trigger the action from a button click or any other event, just like you would do with a function.

Go to the <DeleteInvoice /> component:

src/components/ui/invoices/buttons.tsx

In this code, the deleteInvoiceAction action is triggered when the user clicks the button. The action.submit() method returns a Promise that resolves when the action is done.

Great! Now, we must create a routeAction$(), in src/routes/dashboard/invoices/index.tsx:

src/routes/dashboard/invoices/index.tsx

We can get the id from the data object to the action.

Now, all we need to do is create the deleteInvoice action in the src/lib/actions.ts file:

src/lib/actions.ts

Test it out by deleting an invoice. After clicking the delete button, you should be redirected to the invoices page, and the invoice should be removed.

Congratulations! You've successfully implemented the delete invoice !! 🎉

In the next chapter, you'll learn how to handle errors and display them to the user.

Source code

You can find the source code for chapter 11 on GitHub.

You've Completed Chapter 11

You've learned how to create, update, and delete invoices.

Next Up

12: Handling Errors

Let's explore best practices for mutating data with forms, including error handling and accessibility.