Vaadin 24.4: Introducing the code-first AI-powered UI editor and React seamlessly integrated into the Vaadin platform
Blog

Introducing file-based routing in Vaadin 24.4: Simplify Hilla app creation

By  
Marcus Hellberg
Marcus Hellberg
·
On Jul 8, 2024 1:03:01 PM
·

With the release of Vaadin 24.4, developers building Hilla applications now have access to a powerful new feature: file-based routing. This enhancement streamlines the process of adding new views and configuring view properties directly within your React components. In this blog post, we'll explore how you can leverage file-based routing to reduce boilerplate.

What is file-based routing?

File-based routing in Hilla React applications utilizes .tsx  files in the src/main/frontend/views/ directory and its subdirectories as routes. This means that the directory structure of your view files directly maps to the URL structure of your application. This approach simplifies the process of defining routes and maintaining your application's navigation.

Adding routes

To add a new route, simply create a new .tsx file in the views directory. For example, creating a file named example.tsx with the following content:

export default function ExampleView() {
  return <div>My first view</div>;
}

After saving the file, you can navigate to http://localhost:8080/example to see the new component rendered in the browser. Adding a navigation link to this route is straightforward using the <NavLink> component from react-router-dom:

import { NavLink } from 'react-router-dom';

<NavLink to="/example">My View</NavLink>

This code creates a clickable link that navigates to the /example route.

Filename conventions

The file router supports several naming conventions for .tsx files to handle various routing scenarios:

  • Files starting with _: These are ignored by the file router. For example, _utils.tsx will not be treated as a route.
  • @index.tsx: This file will be mapped to the directory’s index. For example, views/@index.tsx will map to /.
  • @layout.tsx: Defines a layout for the directory's views, wrapping other views in the same directory.
  • {parameter}.tsx: Maps to a required route parameter.
  • .tsx: Maps to an optional route parameter.
  • {...wildcard}.tsx: Maps to a wildcard parameter.

Adding routes with parameters

Creating routes with dynamic parameters is simple. For example, to create a route that accepts a product ID, you can create a file named {productId}.tsx in the views/products/ directory:

import { useParams } from 'react-router-dom';
import { useSignal } from '@hilla/react';
import { useEffect } from 'react';
import { ProductEndpoint } from 'Frontend/generated/endpoints';

export default function ProductView() {
  const { productId } = useParams();
  const product = useSignal(undefined);

  useEffect(() => {
    ProductEndpoint.getProduct(productId).then(p => product.value = p);

  }, [productId]);

  return <div>{product.value?.name}</div>;
}

This route maps to /products/{productId}, where {productId} is a dynamic parameter.

Adding layout routes

Layouts are components that wrap around other views, often used for rendering common elements like toolbars and navigation menus. To add a layout, create a file named @layout.tsx in the views directory:

import { AppLayout } from '@vaadin/react-components/AppLayout';
import { DrawerToggle } from '@vaadin/react-components/DrawerToggle';
import { ProgressBar } from '@vaadin/react-components/ProgressBar';
import { SideNav, SideNavItem } from '@vaadin/react-components/SideNav';
import { Suspense } from 'react';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';

export default function MainLayout() {
  const navigate = useNavigate();
  const location = useLocation();

  return (
    <AppLayout primarySection="drawer">
      <div slot="drawer" className="flex flex-col justify-between h-full p-m">
        <header className="flex flex-col gap-m">
          <h1 className="text-l m-0">My application</h1>
          <SideNav onNavigate={({path}) => path && navigate(path)} location={location}>
            <SideNavItem path="/example">Example</SideNavItem>
          </SideNav>
        </header>
      </div>
      <DrawerToggle slot="navbar" aria-label="Menu toggle"></DrawerToggle>
      <Suspense fallback={<ProgressBar indeterminate />}>
        <Outlet />
      </Suspense>
    </AppLayout>
  );
}

Creating a menu from routes

The structure of application routes is often closely related to the navigation menu. Using the createMenuItems() utility function, you can automatically generate menu items from routes:

import { createMenuItems } from '@vaadin/hilla-file-router/runtime';
import { SideNav, SideNavItem } from '@vaadin/react-components/SideNav';
import { Icon } from '@vaadin/react-components/Icon';
import { useNavigate, useLocation } from 'react-router-dom';

export default function MainMenu() {
  const navigate = useNavigate();
  const location = useLocation();

  return (
    <SideNav onNavigate={({path}) => path && navigate(path)} location={location}>
      {createMenuItems().map(({ to, icon, title }) => (
        <SideNavItem path={to} key={to}>
          {icon && <Icon icon={icon} slot="prefix" />}
          {title}
        </SideNavItem>
      ))}
    </SideNav>
  );
}

Customizing routes

You can customize routes by exporting a config object from your view components. This allows you to set properties like the page title, menu title, and icon:

import { ViewConfig } from "@vaadin/hilla-file-router/types";

export default function AboutView() {
  return <div>About Us</div>;
}

export const config: ViewConfig = {
  title: "About Us",
};

To use this metadata within a component, you can use the useRouteMetadata hook:

import { useRouteMetadata } from "Frontend/util/routing";

export default function MainLayout() {
  const metadata = useRouteMetadata();
  const currentTitle = metadata?.title ?? 'My App';

  useEffect(() => {
    document.title = currentTitle;
  }, [currentTitle]);

  return <div>{/* Your layout code */}</div>;
}

Adding an error page

Adding a custom error page for unmatched routes is straightforward. Create an error view and configure it to match any unknown routes:

export default function ErrorView() {
  return <div>Page not found</div>;
}

export const config: ViewConfig = {
  route: '*',
  menu: {
    exclude: true,
  },
};

With this setup, the error page will be displayed for any unrecognized URL.

Final thoughts

File-based routing in Vaadin 24.4 offers a more intuitive and streamlined way to manage routes in Hilla applications. By mapping your file structure to your URL structure, you can simplify navigation and reduce boilerplate in your routing configuration. This new feature allows you to focus more on developing your application and less on managing routes. For more information, refer to the Hilla routing documentation.

See what's new in Vaadin 24.4!

Marcus Hellberg
Marcus Hellberg
Marcus is the VP of Developer Relations at Vaadin. His daily work includes everything from writing blogs and tech demos to attending events and giving presentations on all things Vaadin and web-related. You can reach out to him on Twitter @marcushellberg.
Other posts by Marcus Hellberg