Query Parameters
In this guide, you’ll learn how to access and set query parameters in a Hilla view using React. A mini-tutorial at the end will help you apply these concepts in a real Vaadin application.
Using Query Parameters
To work with query parameters in Hilla views, you can use the useSearchParams
hook from react-router
. This hook provides methods to read, update, and delete query parameters without causing a full page reload.
The useSearchParams
hook returns an array with two elements:
-
searchParams
— aURLSearchParams
object for reading parameters. -
setSearchParams
— a function to update or clear query parameters.
Here’s an example of a view that manages a search term using query parameters:
import { useSearchParams } from 'react-router';
import { TextField } from '@vaadin/react-components/TextField.js';
export default function ProductView() {
const [searchParams, setSearchParams] = useSearchParams();
const searchTerm = searchParams.get('category') || '';
return (
<div>
<TextField
label="Search for:"
value={searchTerm}
onValueChanged={(e) => {
const newValue = e.detail.value;
if (newValue) {
setSearchParams({ category: newValue });
} else {
setSearchParams({});
}
}}
/>
<div>Current search term: {searchTerm}</div>
</div>
);
}
In this example, the category
parameter is used to store the search term. When a user types a new value, setSearchParams
updates the query parameter accordingly. If the value is empty, passing an empty object to setSearchParams
clears all query parameters.
Multiple Query Parameters
You can work with multiple query parameters simultaneously. In the following example, a view manages both a search filter (category
) and a sorting order (sort
) while ensuring any other query parameters are preserved when updating values:
import { useSearchParams } from 'react-router';
import { TextField } from '@vaadin/react-components/TextField.js';
import { Select } from "@vaadin/react-components";
export default function ProductView() {
const [searchParams, setSearchParams] = useSearchParams();
const category = searchParams.get('category') || '';
const sortOrder = searchParams.get('sort') || 'asc';
// Function to update query parameters while preserving existing ones
const updateParams = (params: Record<string, string>) => {
setSearchParams({
...Object.fromEntries(searchParams),
...params
});
};
// Ensure URL parameters reflect the current state
if (category !== searchParams.get('category')
|| sortOrder !== searchParams.get('sort')) {
updateParams({ category: category, sort: sortOrder });
}
return (
<div>
<TextField
label="Search for:"
value={category}
onValueChanged={(e) => updateParams({ category: e.detail.value })}
/>
<Select
label="Sort order:"
value={sortOrder}
items={[
{ label: 'Ascending', value: 'asc' },
{ label: 'Descending', value: 'desc' },
]}
onValueChanged={(e) => updateParams({ sort: e.detail.value })}
/>
<div>Current search term: {category}</div>
<div>Current sort order: {sortOrder}</div>
</div>
);
}
In this example:
-
The
category
andsort
parameters are managed together in the query string. -
The
updateParams
function ensures that only the changed parameter is updated while preserving any other existing parameters. -
The URL parameters are updated dynamically to reflect changes in search input and sorting selection.
Query Parameters Best Practices
When working with query parameters in Hilla, follow these best practices:
-
Default Values: Determine default values when parameters are missing:
const page = parseInt(searchParams.get('page') || '1'); const size = parseInt(searchParams.get('size') || '10');
-
Type Safety: Convert string parameters to appropriate types:
const isActive = searchParams.get('active') === 'true'; const count = Number(searchParams.get('count'));
-
URL Length: Keep URLs manageable by using concise parameter names and avoiding unnecessary parameters. Extremely long URLs cannot work across all browsers or cannot be handled by all servers.
-
State Management: Use query parameters for shareable state that should persist across page reloads.
-
Security Awareness: Remember that query parameters are visible in the URL and should not contain sensitive information. Thus, never include sensitive data such as security tokens as query parameters, but use HTTP headers (e.g., authorization header), or request body of the post request, or store them in secure cookies.
Try It
In this mini-tutorial, you’ll create a view that accesses and dynamically updates two query parameters.
Set Up the Project
First, generate a walking skeleton with a Hilla UI, open it in your IDE, and run it with hotswap enabled.
Create Browser-Callable Service
Create a new package [application package].tutorial.service
. Then, create a class named ProductService
:
import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.hilla.BrowserCallable;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
@AnonymousAllowed
@BrowserCallable
public class ProductService {
private static final Map<String, List<String>> CATEGORIES = Map.of(
"electronics", List.of("Product 1", "Product 2", "Product 3"),
"clothing", List.of("Product 4", "Product 5", "Product 6"),
"appliances", List.of("Product 7", "Product 8", "Product 9")
);
public List<String> allProducts(String sort) {
return CATEGORIES.values().stream().flatMap(List::stream)
.sorted("asc".equalsIgnoreCase(sort)
? Comparator.naturalOrder()
: Comparator.reverseOrder()).toList();
}
public List<String> productsInCategory(String category, String sort) {
var products = CATEGORIES.get(category);
return products == null ? List.of() :
products.stream()
.sorted("asc".equalsIgnoreCase(sort)
? Comparator.naturalOrder()
: Comparator.reverseOrder()).toList();
}
}
Create the View
Create a view file called products.tsx
under src/main/frontend/views/
:
import { VerticalLayout } from "@vaadin/react-components";
export default function ProductsView() {
return (
<VerticalLayout theme='padding'>
<div>Selected category:</div>
<div>Sort order:</div>
</VerticalLayout>
);
}
Open your browser and navigate to: http://localhost:8080/products
You should see two labels:
-
"Selected category" to show the value of the category query parameter.
-
"Sort order" to show the value of the sort query parameter.
Access Query Parameters
Import and use the useSearchParams
from react-router
to access the query parameters:
import { VerticalLayout } from "@vaadin/react-components";
import { useSearchParams } from 'react-router';
export default function ProductsView() {
const [searchParams, setSearchParams] = useSearchParams();
const rawCategory = searchParams.get('category');
const rawSort = searchParams.get('sort');
return (
<VerticalLayout theme='padding'>
<div>Selected category: <b>{rawCategory}</b></div>
<div>Sort order: <b>{rawSort}</b></div>
</VerticalLayout>
);
}
Try entering the following in the browser’s URL, and see how the values for the query parameters are rendered:
-
Enter
http://localhost:8080/products?category=clothing
and hit the enter key. You should see the literal valueclothing
is displayed in front of the "Selected category:". -
Enter
http://localhost:8080/products?sort=asc
and hit the enter key. You should see the literal valueasc
is displayed in front of the "Sort order:". -
Enter
http://localhost:8080/products?category=appliances&sort=desc
and hit the enter key. You should see the literal values ofappliances
anddesc
are displayed in front of each respective label.
Call the Browser-Callable Service with Query Parameters
Use the query parameters to call the ProductService
methods:
import { VerticalLayout } from "@vaadin/react-components";
import { useSearchParams } from 'react-router';
import { useSignal } from "@vaadin/hilla-react-signals";
import { useEffect } from "react";
import { ProductService } from "Frontend/generated/endpoints";
export default function ProductsView() {
const [searchParams, setSearchParams] = useSearchParams();
const rawCategory = searchParams.get('category');
const rawSort = searchParams.get('sort');
const queryParams = useSignal( 1
{ category: rawCategory || '', sort: rawSort || 'asc' }
);
const products = useSignal<string[]>([]); 2
useEffect(() => {
if (queryParams.value.category === '') {
ProductService.allProducts(queryParams.value.sort) 3
.then((data) => products.value = data);
} else {
ProductService.productsInCategory( 4
queryParams.value.category,
queryParams.value.sort
).then((data) => products.value = data);
}
}, []);
return (
<VerticalLayout theme='padding'>
<div>Current search term: <b>{rawCategory}</b></div>
<div>Current sort order: <b>{rawSort}</b></div>
<br/>
5
<h3>Products from {queryParams.value.category
? `'${queryParams.value.category}' category`
: "all categories"}:
</h3>
<div>
6
<ul>{products.value.map((product) => (
<li key={product}>{product}</li>
))}</ul>
</div>
</VerticalLayout>
);
}
-
Holds the query parameters as a signal, and initializes default values for
category
andsort
, if they are not present. -
Holds the products as a signal to update the view when the products change.
-
Calls the
allProducts
method when thecategory
is empty. -
Calls the
productsInCategory
method when thecategory
is not empty. -
Displays the category name or "all categories" based on the
category
query parameter. -
Renders the list of products based on the
products
signal.
Try entering the following in the browser’s URL, and verify the products are rendered based on the query parameters:
-
Enter
http://localhost:8080/products?category=clothing
and hit the enter key. You should see "Product 4", "Product 5", and "Product 6" are displayed in ascending order. -
Enter
http://localhost:8080/products?sort=asc
and hit the enter key. You should see all the "Product 1" to "Product 9" are displayed in ascending order. -
Enter
http://localhost:8080/products?category=appliances&sort=desc
and hit the enter key. You should see "Product 9", "Product 8", and "Product 7" are displayed (in ascending order).
Update the category
and sort
Query Parameters
Now, update the query parameters dynamically when the user changes the search term or sort order. For this, add a TextField
and a RadioGroup
to the view, and update the query parameters when the user interacts with them:
import {
HorizontalLayout,
RadioButton,
RadioGroup,
TextField,
VerticalLayout
} from "@vaadin/react-components";
import { useSearchParams } from 'react-router';
import { useSignal } from "@vaadin/hilla-react-signals";
import { useEffect } from "react";
import { ProductService } from "Frontend/generated/endpoints";
export default function ProductsView() {
const [searchParams, setSearchParams] = useSearchParams();
const rawCategory = searchParams.get('category');
const rawSort = searchParams.get('sort');
const queryParams = useSignal(
{ category: rawCategory || '', sort: rawSort || 'asc' }
);
const products = useSignal<string[]>([]);
useEffect(() => {
if (rawCategory !== queryParams.value.category
|| rawSort !== queryParams.value.sort) {
setSearchParams({
category: queryParams.value.category,
sort: queryParams.value.sort
});
}
if (queryParams.value.category === '') {
ProductService.allProducts(queryParams.value.sort)
.then((data) => products.value = data);
} else {
ProductService.productsInCategory(
queryParams.value.category,
queryParams.value.sort
).then((data) => products.value = data);
}
}, [queryParams.value]);
return (
<VerticalLayout theme='padding'>
<HorizontalLayout theme='spacing padding'>
<TextField
label="Category:"
value={queryParams.value.category}
onValueChanged={(e) => {
const newValue = e.detail.value;
if (newValue) {
queryParams.value = {
category: newValue,
sort: queryParams.value.sort
};
} else {
queryParams.value = {
category: '',
sort: queryParams.value.sort
};
}
}}
/>
<RadioGroup label="Sort order:"
onValueChanged={(event) => queryParams.value = {
category: queryParams.value.category,
sort: event.detail.value
}
}>
<RadioButton value="asc"
checked={queryParams.value.sort === 'asc'}
label='Ascending'/>
<RadioButton value="desc"
checked={queryParams.value.sort === 'desc'}
label='Descending'/>
</RadioGroup>
</HorizontalLayout>
<div>Current search term: <b>{rawCategory}</b></div>
<div>Current sort order: <b>{rawSort}</b></div>
<br/>
<h3>Products from {queryParams.value.category
? `'${queryParams.value.category}' category`
: "all categories"}:
</h3>
<div>
<ul>{products.value.map((product) => (
<li key={product}>{product}</li>
))}</ul>
</div>
</VerticalLayout>
);
}
Verify the following:
-
Same as before, try entering the following in the browser’s URL, and verify the text field and the radio group values are in harmony with query parameters, as well as the rendered products:
-
Enter
http://localhost:8080/products?category=clothing
and hit the enter key. You should see "Product 4", "Product 5", and "Product 6" are displayed in ascending order. -
Enter
http://localhost:8080/products?sort=asc
and hit the enter key. You should see all the "Product 1" to "Product 9" are displayed in ascending order. -
Enter
http://localhost:8080/products?category=appliances&sort=desc
and hit the enter key. You should see "Product 9", "Product 8", and "Product 7" are displayed (in ascending order).
-
-
Try changing the text field value and the radio group value in the browser. Verify that the URL query parameters are updated automatically.
-
Try adding the functionality of showing a warning message when users try to enter nonexistent categories, and show all the products instead of an empty list. (optional)
Final Thoughts
You’ve now successfully implemented query parameters in Hilla using React. You learned how to:
-
Access query parameter values.
-
Use query parameters to call Browser-Callable services.
-
Update query parameter values dynamically.
You’re now ready to use query parameters in real Vaadin applications!