Docs

Documentation versions (currently viewingVaadin 24)

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 — a URLSearchParams 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 and sort 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:

  1. Default Values: Determine default values when parameters are missing:

    const page = parseInt(searchParams.get('page') || '1');
    const size = parseInt(searchParams.get('size') || '10');
  2. Type Safety: Convert string parameters to appropriate types:

    const isActive = searchParams.get('active') === 'true';
    const count = Number(searchParams.get('count'));
  3. 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.

  4. State Management: Use query parameters for shareable state that should persist across page reloads.

  5. 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:

  1. "Selected category" to show the value of the category query parameter.

  2. "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:

  1. Enter http://localhost:8080/products?category=clothing and hit the enter key. You should see the literal value clothing is displayed in front of the "Selected category:".

  2. Enter http://localhost:8080/products?sort=asc and hit the enter key. You should see the literal value asc is displayed in front of the "Sort order:".

  3. Enter http://localhost:8080/products?category=appliances&sort=desc and hit the enter key. You should see the literal values of appliances and desc 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>
    );
}
  1. Holds the query parameters as a signal, and initializes default values for category and sort, if they are not present.

  2. Holds the products as a signal to update the view when the products change.

  3. Calls the allProducts method when the category is empty.

  4. Calls the productsInCategory method when the category is not empty.

  5. Displays the category name or "all categories" based on the category query parameter.

  6. 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:

  1. 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.

  2. 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.

  3. 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:

  1. 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:

  2. Try changing the text field value and the radio group value in the browser. Verify that the URL query parameters are updated automatically.

  3. 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!