10
Chapter 10
Adding Search and Pagination
In the previous chapter, you improved your dashboard's initial loading performance with streaming. Now let's move on to the /invoices
page, and learn how to add search and pagination!
In this chapter...
Here are the topics we will cover:
Learn how to use the Qwik-City APIs: useLocation() and useNavigate()
Implement search and pagination using URL search params.
Starting code
First, you must download 'invoices.zip'
folder, unzip and place them in the 'src/components/ui'
folder:
Second, you must download 'button.tsx'
and search.tsx
files and place them in the 'src/components/ui'
folder:
Third, inside your 'src/routes/dashboard/invoices/index.tsx'
file, paste the following code:
Spend some time familiarizing yourself with the page and the components you'll be working with:
<Search />
allows users to search for specific invoices.<Pagination />
allows users to navigate between pages of invoices.<Table />
displays the invoices.
Your search functionality will span the client and the server. When a user searches for an invoice on the client, the URL params will be updated, data will be fetched on the server, and the table will re-render on the server with the new data.
Why use URL search params?
As mentioned above, you'll be using URL search params to manage the search state. This pattern may be new if you're used to doing it with client side state.
There are a couple of benefits of implementing search with URL params:
- Bookmarkable and Shareable URLs: Since the search parameters are in the URL, users can bookmark the current state of the application, including their search queries and filters, for future reference or sharing.
- Server-Side Rendering and Initial Load: URL parameters can be directly consumed on the server to render the initial state, making it easier to handle server rendering.
- Analytics and Tracking: Having search queries and filters directly in the URL makes it easier to track user behavior without requiring additional client-side logic.
Adding the search functionality
These are the Qwik-City functions that you'll use to implement the search functionality:
useLocation()
- Provides the current URL and params. It also determines if the app is currently navigating, which is useful for showing a loading indicator.useNavigate()
- Returns a function that programmatically navigates to the next page without requiring a user click or causing a full-page reload. This function can be called with a string argument to "push" a new path, or without arguments to cause an SPA refresh of the page. This is the API used by the<Link>
component internally to support SPA navigation.
Here's a quick overview of the implementation steps:
- Capture the user's input.
- Update the URL with the search params.
- Keep the URL in sync with the input field.
- Update the table to reflect the search query.
1. Capturing the user's input
Go into the <Search>
Component (/app/ui/search.tsx
):
Create a new handleSearch
function, and add an onInput$
listener to the <input>
element. onInput$
will invoke handleSearch
whenever the input value changes.
Test that it's working correctly by opening the console in your Developer Tools, then type into the search field. You should see the search term logged to the console.
Great! You're capturing the user's search input. Now, you need to update the URL with the search term.
2. Updating the URL with the search params
Import the useLocation()
function from @builder.io/qwik-city
, and assign a variable.
The 'loc'
variable now contains the current URL and search params. You can use this for acces to the searchParams.
Inside handleSearch,
create a new URLSearchParams instance using your new searchParams
variable.
URLSearchParams
is a Web API that provides utility methods for manipulating the URL query parameters. Instead of creating a complex string literal, you can use it to get the params string like ?page=1&query=a
.
Next, set
the params string based on the user’s input. If the input is empty, you want to delete
it:
Now that you have the query string. You can recover the pathname with 'loc'
variable and use useNavigate()
function to update the URL.
Import useNavigate()
from @builder.io/qwik-city
and use the nav()
method inside handleSearch()
:
Here's a breakdown of what's happening:
${pathname}
is the current URL path, in your cas,"dashboard/invoices
- As the user types into the search bar,
params.toString()
translates this input into a URL-friendly format. nav(${pathname}?${params.toString()})
updates the URL with the user's search data. For example,/dashboard/invoices?query=lee
if the user searches for "Lee".- The URL is updated without reloading the page, thanks to Qwik SPA navigation (which you learned about in the chapter on navigating between pages).
- The
`replaceState: true`
option in the `nav` function is used to replace the current state in the browser's history instead of adding a new entry. This means that when the URL is updated with the search term, the current navigation does not add a new history entry. This is particularly useful for dynamic URL updates, such as search filters or pagination parameters, where you do not want to clutter the browser's history with each change. Thus, if the user presses the back button, they won't go through each previous search step but directly return to the page they were on before performing the search.
3. Keeping the URL and input in sync
To ensure the input field is in sync with the URL and will be populated when sharing, you can pass a defaultValue
to input by reading from searchParams
:
defaultValue
vs.value
/ Controlled vs. UncontrolledIn Qwik, if you want to ensure that the input field reflects the current URL parameters and updates accordingly, you should use the
value
attribute. This makes the input a controlled component, ensuring that Qwik manages the input's state based on the URL parameters.Using
defaultValue
only sets the initial value of the input when the component is first rendered. It does not update the input value when the URL parameters change. Therefore, using value is necessary to keep the input in sync with the URL parameters.
4. Updating the table to reflect the search query
Finally, you need to update the table component to reflect the search query.
Navigate back to the invoices page.
In src/components/ui/invoices/table.tsx
component:
- Import
useLocation()
from@builder.io/qwik-city
and attribute it to a variable. - Import
Resource
anduseResource$
from@builder.io/qwik
- Use
useResource$
for fetching the invoices data. - Use
Resource
component for rendering the invoices data. - Import
InvoicesTableSkeleton
use whenResource
isonPending
Here's the updated table.tsx
component:
We miss him fetchFilteredInvoices()
function that fetches the invoices data. You can create this function at the end of the lib/data.ts
file:
With these changes in place, go ahead and test it out. If you search for a term, you'll update the URL, which will send a new request to the server, data will be fetched on the server, and only the invoices that match your query will be returned.
Best practice: Debouncing
Congratulations! You've implemented search with Qwik! But there's something you can do to optimize it.
Inside your handleSearch
function, add the following console.log
:
Then type "Emil" into your search bar and check the console in dev tools. What is happening?
You're updating the URL on every keystroke, and therefore querying your database on every keystroke! This isn't a problem as our application is small, but imagine if your application had thousands of users, each sending a new request to your database on each keystroke.
Debouncing is a programming practice that limits the rate at which a function can fire. In our case, you only want to query the database when the user has stopped typing.
How Debouncing Works:
- Trigger Event: When an event that should be debounced (like a keystroke in the search box) occurs, a timer starts.
- Wait: If a new event occurs before the timer expires, the timer is reset.
- Execution: If the timer reaches the end of its countdown, the debounced function is executed.
For this, we will create our own hook: useDebouncer()
In the src/
folder create /hooks/
folder.
Inside the hooks/
folder, create a new file called debouncer.ts
Here's the code for the debouncer.ts
file:
This hook will take two arguments: the function you want to debounce and the delay in milliseconds. It will return a new function that will debounce the original function.
Now, import useDebouncer
into your search.tsx
file:
Now, when you type into the search bar, you'll see that the console only logs the search term once you've stopped typing for 300 milliseconds.
By debouncing, you can reduce the number of requests sent to your database, thus saving resources.
It’s time to take a quiz!
Test your knowledge and see what you’ve just learned.
What problem does debouncing solve in the search feature?
Adding pagination
After introducing the search feature, you'll notice the table displays only 6 invoices at a time. This is because the fetchFilteredInvoices()
function in data.ts
returns a maximum of 6 invoices per page.
Adding pagination allows users to navigate through the different pages to view all the invoices. Let's see how you can implement pagination using URL params, just like you did with search.
At the end src/lib/data
file paste the following code:
This function will return the total number of pages based on the search query. For example, if there are 12 invoices that match the search query, and each page displays 6 invoices, then the total number of pages would be 2.
Now, in the src/components/ui/invoices/pagination.tsx
file, import:
useLocation()
from@builder.io/qwik-city
Resource
anduseResource$
from@builder.io/qwik
fetchInvoicesPages()
function fromlib/data
generatePagination()
function fromsrc/lib/utils
We use useResource$
to fetch the total number of pages and Resource
to render the pagination.
Here's the updated pagination.tsx
file:
The changes made to the pagination aim to make the component more dynamic and synchronize it with the URL parameters. Here is a summary of the main modifications:
useLocation
: Allows access to URL parameters to determine the current page and search query. This synchronizes the URL state with the component.currentPage
constant determined the current page from thepage
parameter.useResource$
: Facilitates asynchronous data fetching (such as the total number of pages) based on the current search parameters. This makes the component more dynamic and responsive to URL changes.Resource
: Manages conditional rendering based on the resource state (loading, success, error). This allows displaying the pagination only when the necessary data is available.generatePagination
: Generates a list of pages to display in the pagination. This function is called after successfully fetching the total number of pages.PaginationArrow
andPaginationNumber
: Individual components to display navigation arrows and page numbers, respectively. Their state is determined based on the current page and total number of pages.createPageURL
: Dynamically constructs the pagination links based on the current page and URL parameters, ensuring smooth navigation between pages.
The last function, createPageURL
, do not exist yet. We will create it in the next steps. 👇
Here's a breakdown of what's happening:
createPageURL
creates an instance of the current search parameters.- Then, it updates the "page" parameter to the provided page number.
- Finally, it constructs the full URL using the pathname and updated search parameters.
The rest of the <Pagination>
component deals with styling and different states (first, last, active, disabled, etc). We won't go into detail for this course, but feel free to look through the code to see where createPageURL
is being called.
Finally, when the user types a new search query, you want to reset the page number to 1. You can do this by updating the handleSearch
function in your <Search>
component:
Summary
Congratulations! You've just implemented search and pagination using URL Params and Next.js APIs.
To summarize, in this chapter:
- You've handled search and pagination with URL search parameters instead of client state.
- You've fetched data asynchronously using Qwik's
useResource$
. - You're using the
useLocation
hook for smoother, client-side transitions. - You've implemented debouncing to optimize the search feature.
- You've dynamically constructed pagination links using the
createPageURL
function. - You've managed conditional rendering based on the resource state with Qwik's
Resource
component.
These patterns differ from what you may be used to when working with client-side React, but hopefully, you now better understand the benefits of using URL search params and lifting this state to the server.
Well done !
Source code
You can find the source code for chapter 10 on GitHub.
You've Completed Chapter 10
You've learned how to add search and pagination to your app.
Next Up
11: Mutating data
Learn how to update data in your database.
https://github.com/DevWeb13/learn-qwik/issues/23