TanStack Router Tutorial

Steps in building the project from the starter repo

Setting up the routes

  • download the starter repo
  • npm install
  • create a folder named routes inside the src folder
  • create a file named __root.tsx inside the routes folder
import { createRootRoute } from "@tanstack/react-router";

export const Route = createRootRoute({
    component: RootComponent
});

function RootComponent() {
    return (
        <div>
            <h1>Hello from React and TanStack Router</h1>
        </div>
    );
}
  • import and add TanStackRouterVite() in vite.config.ts's plugins array
  • install TanStack Router CLI
npm i -g @tanstack/router-cli
  • run the TanStack Router CLI to generate routeTree.gen.ts file
tsr generate

The tsr generate command will generate a routeTree.gen.ts file in the src folder. Normally in React Router, we would have to manually create the routes and nested routes. But with TanStack Router, we can generate the routes and nested routes using the CLI.

  • run the tsr watcher to watch for changes in the routeTree.gen.ts file
tsr watch
  • run the project
npm run dev
  • finish the App.tsx file to integrate the typesafe routes.
  • add an index.tsx file inside the routes folder. This file will be the entry point for the routes and boilerplate will be generated by the CLI for you.
  • update the __root.tsx file to add the Outlet component.
<>
  <div id="detail">
    <Outlet />
  </div>
</>
  • see the changes in the browser

Adding the TanStack Router Devtools

  • update the __root.tsx file to add the TanStackRouterDevtools component.
const TanStackRouterDevtools =
  process.env.NODE_ENV === 'production'
    ? () => null // Render nothing in production
    : lazy(() =>
        // Lazy load in development
        import('@tanstack/router-devtools').then((res) => ({
          default: res.TanStackRouterDevtools,
          // For Embedded Mode
          // default: res.TanStackRouterDevtoolsPanel
        }))
      );
  • then above the RootComponent function, add the following code:
<>
  <div id="detail">
    <Outlet />
  </div>
  <Suspense>
    <TanStackRouterDevtools position="bottom-right" />
  </Suspense>
</>
  • see the changes in the browser

Adding New Contact functionality

  • update the __root.tsx file to add sidebar and the list of contacts.
<>
  <div id="sidebar">
    <SidebarFooter />
  </div>
  <div id="detail">
    <Outlet />
  </div>
  <Suspense>
    <TanStackRouterDevtools position="bottom-right" />
  </Suspense>
</>
  • import and place the <SidebarSearchContact /> below the SideBarFooter component.
<SidebarSearchContact />
  • open the chrome devtools and go to the Application tab
  • go to the indexedDB of the application
  • go the web app and click the New button that will trigger the createContact function.
  • confirm that the new contact is added to the indexedDB

Showing the list of contacts

  • update the RootRouteOptions of the __root.tsx file.
export const Route = createRootRoute({
  component: RootComponent,
  validateSearch: z.object({
    q: z.string().optional(),
  }),
  // eslint-disable-next-line sort-keys-fix/sort-keys-fix
  loaderDeps: ({ search: { q } }) => {
    return { q };
  },
  // eslint-disable-next-line sort-keys-fix/sort-keys-fix
  loader: async ({ deps: { q } }) => {
    const contacts = (await getContacts(q || '')) as Contact[];

    return { contacts, q };
  },
});
  • add the <SidebarContactList /> below the SidebarSearchContact component.
<SidebarContactList />
  • see the changes in the browser. It says no contacts.
  • replace the placeholder of contacts with the Route instance from the __root.tsx file.
const { contacts } = Route.useLoaderData();
  • see the changes in the browser. It should now show the list of contacts with one object (no name).

Creating a navigation functionality

  • create a new page named contacts.$contactId.index.tsx inside the routes folder then save the file to reload your IDE.
  • check the routeTree.gen.ts file. It should have the new route.
  • change the ahref tag to use the Link component from TanStack Router.
<Link to={`/contacts/`}></Link>
  • hover over the to prop and see the type of the prop. It should be a set of union type of strings.
  • update the Link with this.
<Link to={`/contacts/${contact.id}`}></Link>
  • go to the browser and click on the contact. It should navigate to the empty contact detail page.

Adding the Contact Detail functionality

  • update the contacts.$contactId.index.tsx file to show the contact details.
import { createFileRoute, notFound } from '@tanstack/react-router';
import { z } from 'zod';

import { getContact } from '../services/contacts';
import ContactDetail from '../components/ContactDetail';
import NotFoundPage from '../components/NotFoundPage';
import ErrorPage from '../components/ErrorPage';

export const Route = createFileRoute('/contacts/$contactId/')({
  component: () => <div>Hello /contacts/$contactId/!</div>,
  notFoundComponent: () => <NotFoundPage message={"Can't find contact"} />,
  errorComponent: () => <ErrorPage message={'Network error'} />,
  params: {
    parse: (params) => {
      return {
        contactId: z.string().parse(params.contactId),
      };
    },
    stringify: ({ contactId }) => {
      return { contactId: `${contactId}` };
    },
  },
  // eslint-disable-next-line sort-keys-fix/sort-keys-fix
  loader: async ({ params: { contactId } }) => {
    const contact = await getContact(contactId as string);
    if (!contact) {
      throw notFound({ _global: false });
    }

    return contact;
  },
});
  • In the same file, separate the component and import the ContactDetail component like this:
function ContactIdIndexComponent() {
  return (<ContactDetail />);
}
  • Still in the same file, update the component prop of the Route instance to use the ContactIdIndexComponent function.
  component: ContactIdIndexComponent,
  • go to the ContactDetail.tsx and replace the contact placeholder with the Route instance from the contacts.$contactId.index.tsx file.
const contact = Route.useLoaderData();

Adding delete functionality

  • replace the params placeholder with this:
const params = Route.useParams();
  • add the the Route's useNavigate hook to the component.
const navigate = Route.useNavigate();
  • update the handleDelete function to use the params and navigate function.
  const handleDeleteEvent = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (globalThis.confirm('Please confirm you want to delete this record.')) {
      await deleteContact(params.contactId);
      await navigate({
        to: '/',
      });
    }
  };
  • go to the browser and click on the delete button. It should delete the contact and navigate to the home page.
  • add more contacts and delete them to see the changes in the browser.

Adding the Edit functionality

  • create a new page named contacts.$contactId.edit.tsx inside the routes folder then save the file to reload your IDE.
  • update the contacts.$contactId.edit.tsx file to fetch the contact details.
import { createFileRoute, notFound } from '@tanstack/react-router';
import { getContact } from '../services/contacts';
import EditContactForm from '../components/EditContactForm';

export const Route = createFileRoute('/contacts/$contactId/edit')({
  component: () => <div>Hello /contacts/$contactId/edit!</div>,
  loader: async ({ params: { contactId } }) => {
    const contact = await getContact(contactId);
    if (!contact) {
      throw notFound({ _global: false });
    }
    return contact;
  },
});
  • In the same file, separate the component and import the EditContactForm component like this:
function EditContactComponent() {
  return (<EditContactForm />);
}
  • Still in the same file, update the component prop of the Route instance to use the EditContactComponent function.
  component: EditContactComponent,
  • we will need again the 3 Route hooks. Place them in the EditContactForm.tsx file.
  const contact = Route.useLoaderData();
  const params = Route.useParams();
  const navigate = Route.useNavigate();
  • update the handleOnSubmit function to the params and the navigate.
  const handleOnSubmit = async (event: FormEvent) => {
    event.preventDefault();
    const form = event.currentTarget as HTMLFormElement;
    const formData = new FormData(form);
    const updates = Object.fromEntries(formData.entries());
    await updateContact(params.contactId as string, updates);
    await navigate({
      to: `/contacts/${params.contactId}`,
    });
  };
  • Go the browser and click a no name contact. Add a /edit to the url and see the edit form.
  • Try to update the details of the contact and see the changes in the browser.

Adding navigation going to the edit form of the contact

  • go back to the ContactDetail.tsx file and update the handleEditEvent function with this:
  const handleEditEvent = async (event: FormEvent) => {
    event.preventDefault();
    await navigate({
      to: `/contacts/${params.contactId}/edit`,
    });
  };
  • go back also to the SidebarSearchContact.tsx file. Import the Route from the __root.tsx and add this hook to the component.
  const navigate = Route.useNavigate();
  • update the handleOnSubmit with this logic.
const contact = await createContact();
    await navigate({
      to: `/contacts/${contact.id}/edit`,
    });
  • add this inside the onClick event of the cancel button.
 navigate({
              to: `/contacts/${contact.id}`,
            })
  • go to the browser and click the New button. It should navigate to the edit form of the new contact.

Adding search functionality

  • go to the __root.tsx file and the hooks:
  const { q } = Route.useLoaderData();
  const [query, setQuery] = useState(q ?? '');
  const router = useRouter();

  useEffect(() => {
    if (q) setQuery(q);
  }, [q]);
  • update the div tag of the <Outlet /> with this:
<div id="detail" className={router.state.isLoading ? 'loading' : ''}>
        <Outlet />
</div>
  • add query and setQuery props to the SidebarSearchContact component.
<SidebarSearchContact query={query} setQuery={setQuery} />
  • go to the SidebarSearchContact.tsx file and add the props to the component.
function SidebarSearchContact({ query, setQuery }: Props)
  • update the handleOnChangeEvent function with this:
  const handleOnChangeEvent = async (e: FormEvent<HTMLInputElement>) => {
    setQuery(e.currentTarget.value);
    await navigate({ search: { q: e.currentTarget.value } });
  };
  • import the useRouter hook to the component.
  const router = useRouter();
  • add two more properties to the input tag.
value={query}
className={router.state.isLoading ? 'loading' : ''}
  • replace the hidden property of the div tag with an id id="search-spinner" with this:
hidden={!router.state.isLoading}
  • go to the browser and type a name in the search bar. It should show the loading spinner and the list of contacts.

Adding the star functionality

  • go to the Favorite.tsx file and add the useRouter hook.
const router = useRouter();
  • then update the onSubmit function with this after or below the updateContact function. The invalidation will trigger the loader to fetch the data again.:
await router.invalidate();

End of the TanStack Router Tutorial

  • Thank you for following the steps. You can now explore more of the TanStack Router features and functionalities.
  • You can also check the TanStack Router documentation for more information.
  • Happy coding! 🚀