11
Chapter 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:
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?
Let's see how it all works together!
Creating a new invoice
Here are the steps you'll take to create a new invoice:
- Create a form to capture the user's input.
- Create a routeAction$()
- Check the data from the form.
- Validate the data.
- Prepare and insert the data into the database.
- 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:
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:
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 withtype="number"
. - Has two
<input>
elements for the status withtype="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:
On http://localhost:5173/dashboard/invoices/create, you should see the following UI:
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
:
Now, in your <CreateForm>
component import and use the useCreateInvoice
action:
3. Check the data from the form
Back in your src/routes/dashboard/invoices/create/index.tsx
and add this log:
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:
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
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.
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
:
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:
Creating new dates
Finally, let's create a new date with the format "YYYY-MM-DD" for the invoice's creation date:
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:
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.
Now, all we need to do is import and use our createInvoice()
function in our routeAction$()
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.
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:
- You should be redirected to the
/dashboard/invoices
route on submission. - 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:
- Create a new dynamic route segment with the invoice
id
. - Read the invoice
id
from the URL params. - Fetch the specific invoice from your database.
- Pre-populate the form with the invoice data.
- 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:
In your <Table>
component, you'll need to update the two <UpdateInvoice />
buttons (mobile and desktop) to pass the invoice id
as a parameter.
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:
2. Read the invoice id from the URL params
Back on yoursrc/routes/dashboard/invoices/edit/index.tsx
file, paste the following code:
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:
3. Fetch the specific invoice
Then:
- Import a new function called
fetchInvoiceById
and pass theid
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:
In your src/lib/data.ts
file, create a new function to fetch the invoice by id
:
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:
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:
In your form:
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:
- Check the data with
Zod
before inserting it into the database. - Create a new action called
updateInvoice
to update the invoice in the database. - Redirect the user back to the invoices page after successfully updating the invoice.
In your src/routes/dashboard/invoices/[id]/edit/index.tsx
file:
In your src/lib/actions.ts
file, create a new action called updateInvoice
:
Redirect the user back to the invoices page after updating the invoice:
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:
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:
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
:
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:
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.
https://github.com/DevWeb13/learn-qwik/issues/23