Building a Resource Management View
In this tutorial, you will build a fully functional resource management dashboard using the pre-built components and services in this template. You will learn how to fetch data using the generated SDK, display it in a data table, and implement CRUD (Create, Read, Update, Delete) operations.
By the end of this guide, you will have a dashboard that lists items, allows for new item creation, and provides a menu for editing or deleting existing entries.
Prerequisites
Before starting, ensure you have the following configured in your project:
- TanStack Query: For data fetching and state management.
- TanStack Router: For route handling.
- Generated SDK: The
ItemsServicemust be generated and available infrontend/src/client.
Step 1: Fetching Data with ItemsService
The first step is to retrieve your data from the backend. You will use the ItemsService.readItems method within a TanStack Query.
In frontend/src/routes/_layout/items.tsx, define your query options and use the useSuspenseQuery hook:
import { useSuspenseQuery } from "@tanstack/react-query"
import { ItemsService } from "@/client"
function getItemsQueryOptions() {
return {
queryFn: () => ItemsService.readItems({ skip: 0, limit: 100 }),
queryKey: ["items"],
}
}
function ItemsTableContent() {
// This hook automatically handles the loading state via React Suspense
const { data: items } = useSuspenseQuery(getItemsQueryOptions())
return <DataTable columns={columns} data={items.data} />
}
The ItemsService.readItems method is a static call that returns a CancelablePromise. By wrapping it in getItemsQueryOptions, you create a reusable configuration that TanStack Query uses to manage caching and background updates.
Step 2: Defining the Table Structure
To display the fetched data, you need to define how each column should look. This is handled in frontend/src/components/Items/columns.tsx using the ColumnDef type from @tanstack/react-table.
import type { ColumnDef } from "@tanstack/react-table"
import type { ItemPublic } from "@/client"
import { ItemActionsMenu } from "./ItemActionsMenu"
export const columns: ColumnDef<ItemPublic>[] = [
{
accessorKey: "title",
header: "Title",
cell: ({ row }) => (
<span className="font-medium">{row.original.title}</span>
),
},
{
accessorKey: "description",
header: "Description",
cell: ({ row }) => (
<span className="max-w-xs truncate block text-muted-foreground">
{row.original.description || "No description"}
</span>
),
},
{
id: "actions",
cell: ({ row }) => (
<div className="flex justify-end">
<ItemActionsMenu item={row.original} />
</div>
),
},
]
In this configuration, the actions column is used to inject the ItemActionsMenu component into every row, passing the current item data as a prop.
Step 3: Implementing Row Actions
The ItemActionsMenu component provides a dropdown for managing individual resources. It acts as a container for EditItem and DeleteItem components.
In frontend/src/components/Items/ItemActionsMenu.tsx:
import { useState } from "react"
import { EllipsisVertical } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { EditItem } from "./EditItem"
import { DeleteItem } from "./DeleteItem"
export const ItemActionsMenu = ({ item }: ItemActionsMenuProps) => {
const [open, setOpen] = useState(false)
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<EllipsisVertical />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<EditItem item={item} onSuccess={() => setOpen(false)} />
<DeleteItem id={item.id} onSuccess={() => setOpen(false)} />
</DropdownMenuContent>
</DropdownMenu>
)
}
This component uses a local open state to control the dropdown. When an action like "Edit" or "Delete" succeeds, the onSuccess callback closes the menu.
Step 4: Creating New Resources
To add new items to your dashboard, use the AddItem component. This component combines a Shadcn UI Dialog, react-hook-form for validation, and a TanStack Query mutation.
In frontend/src/components/Items/AddItem.tsx, the mutation is defined as follows:
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (data: ItemCreate) =>
ItemsService.createItem({ requestBody: data }),
onSuccess: () => {
showSuccessToast("Item created successfully")
form.reset()
setIsOpen(false)
},
onError: handleError.bind(showErrorToast),
onSettled: () => {
// This is critical for UI synchronization
queryClient.invalidateQueries({ queryKey: ["items"] })
},
})
const onSubmit = (data: FormData) => {
mutation.mutate(data)
}
When the form is submitted, ItemsService.createItem sends a POST request to the API. The onSettled callback ensures that the ["items"] query is invalidated, forcing the table to refetch and display the newly created item immediately.
Step 5: Final Assembly
Finally, combine these components in your main route component in frontend/src/routes/_layout/items.tsx:
function Items() {
return (
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight">Items</h1>
<p className="text-muted-foreground">Create and manage your items</p>
</div>
<AddItem />
</div>
<Suspense fallback={<PendingItems />}>
<ItemsTableContent />
</Suspense>
</div>
)
}
By wrapping ItemsTableContent in Suspense, you provide a smooth loading experience while ItemsService.readItems is fetching data. The AddItem button is placed at the top level, providing a clear entry point for resource creation.
Summary of the Pattern
This implementation follows a consistent pattern used throughout the template:
- Service Layer:
ItemsServicehandles all raw API communication. - Query Layer: TanStack Query (
useSuspenseQuery,useMutation) manages the server state. - UI Layer: Shadcn UI components (
DataTable,Dialog,DropdownMenu) provide the visual structure. - Synchronization:
queryClient.invalidateQueriesensures the UI stays in sync with the backend after any modification.