---
title: "Complete Data Table with TanStack Table in Next.js"
slug: "build-data-table-tanstack-shadcn-nextjs"
published: "2026-02-24"
updated: "2026-04-06"
categories:
  - "Next.js"
tags:
  - "TanStack Table"
  - "shadcn/ui"
  - "Next.js"
  - "TanStack React Table"
  - "build data table in Next.js"
  - "sortable filterable table"
  - "custom cell renderers"
  - "useReactTable"
  - "SerializedCostObject"
  - "App Router data fetching"
  - "pagination"
  - "server-client boundary"
llm-intent: "how-to"
audience-level: "intermediate"
llm-purpose: "TanStack Table: Build a production-ready Data Table in Next.js with shadcn/ui — add sorting, filtering, pagination, and custom cells. Follow the…"
llm-prereqs:
  - "@tanstack/react-table"
  - "shadcn/ui"
  - "Next.js (App Router)"
  - "TypeScript"
  - "React"
  - "Lucide"
  - "Tailwind CSS"
  - "Payload CMS (example)"
---

**Summary Triples**
- (Complete Data Table with TanStack Table in Next.js, expresses-intent, how-to)
- (Complete Data Table with TanStack Table in Next.js, covers-topic, TanStack Table)
- (Complete Data Table with TanStack Table in Next.js, provides-guidance-for, TanStack Table: Build a production-ready Data Table in Next.js with shadcn/ui — add sorting, filtering, pagination, and custom cells. Follow the…)

### {GOAL}
TanStack Table: Build a production-ready Data Table in Next.js with shadcn/ui — add sorting, filtering, pagination, and custom cells. Follow the…

### {PREREQS}
- @tanstack/react-table
- shadcn/ui
- Next.js (App Router)
- TypeScript
- React
- Lucide
- Tailwind CSS
- Payload CMS (example)

### {STEPS}
1. Define serializable data shape
2. Create basic column definitions
3. Build minimal DataTable component
4. Implement server page and serialization
5. Add column sorting support
6. Add column filtering UI
7. Enable pagination controls
8. Create custom cell renderers
9. Extract a reusable toolbar

<!-- llm:goal="TanStack Table: Build a production-ready Data Table in Next.js with shadcn/ui — add sorting, filtering, pagination, and custom cells. Follow the…" -->
<!-- llm:prereq="@tanstack/react-table" -->
<!-- llm:prereq="shadcn/ui" -->
<!-- llm:prereq="Next.js (App Router)" -->
<!-- llm:prereq="TypeScript" -->
<!-- llm:prereq="React" -->
<!-- llm:prereq="Lucide" -->
<!-- llm:prereq="Tailwind CSS" -->
<!-- llm:prereq="Payload CMS (example)" -->

# Complete Data Table with TanStack Table in Next.js
> TanStack Table: Build a production-ready Data Table in Next.js with shadcn/ui — add sorting, filtering, pagination, and custom cells. Follow the…
Matija Žiberna · 2026-02-24

In the previous article, I compared shadcn's static `<Table>` with a TanStack-powered Data Table and laid out when each one makes sense. This article is the implementation side. We're going to build a Data Table from scratch, adding one feature at a time, so you understand what each piece does and can stop wherever your requirements end.

By the end, you'll have a fully working Data Table with sortable columns, a filter input, pagination, custom cell renderers, and a clean toolbar — all built on top of the same shadcn `<Table>` component you already know.

I'm using a cost objects dataset as the running example — rows with a code, name, type, description, and status — but the patterns apply to any data you're working with.

## Prerequisites

You need two things installed. First, shadcn's Table component:

```bash
npx shadcn@latest add table
```

Second, TanStack React Table:

```bash
npm install @tanstack/react-table
```

If you're following along in a Next.js App Router project, you'll also want shadcn's `<Button>`, `<Input>`, and `<Badge>` components available. We'll use them for sorting controls, the filter input, and status cells.

## The Three-File Architecture

Before writing any code, let's set up the file structure. A Data Table splits into three files, and this split isn't a stylistic choice — it follows the server/client boundary in Next.js App Router.

```
src/
├── app/(frontend)/cost-objects/
│   └── page.tsx              ← Server component: fetches data
└── components/cost-objects/
    ├── types.ts              ← Shared type definition
    ├── columns.tsx           ← Client component: column definitions
    └── CostObjectsDataTable.tsx  ← Client component: table rendering
```

The page is a server component. It fetches data from your database or API and passes it as a prop. The columns file and the DataTable component are client components because they use React hooks and event handlers. This separation means your data-fetching logic never ships to the browser, while the interactive table logic runs entirely on the client.

Let's build each file, starting from the simplest working version and layering features on top.

## Step 1: Define Your Data Shape

Start by defining the shape of the data your table will display. This type is shared between the server page (which produces the data) and the client components (which consume it).

```tsx
// File: src/components/cost-objects/types.ts
export interface SerializedCostObject {
  id: number;
  code: string;
  name: string;
  typeName: string | null;
  description: string | null;
  status: string;
}
```

I prefix the type with "Serialized" deliberately. If you're using an ORM or CMS like Payload, your database objects contain methods, circular references, and relationship objects that can't be passed from a server component to a client component. You need a plain, serializable version. Defining this type explicitly makes that boundary visible in your code.

## Step 2: Basic Column Definitions

The columns file is where you define what your table displays. Each entry in the `columns` array maps to one column in the table. At its simplest, a column just needs an `accessorKey` that matches a property on your data type, and a `header` string.

```tsx
// File: src/components/cost-objects/columns.tsx
"use client";

import { ColumnDef } from "@tanstack/react-table";
import type { SerializedCostObject } from "./types";

export const columns: ColumnDef<SerializedCostObject>[] = [
  {
    accessorKey: "code",
    header: "Code",
  },
  {
    accessorKey: "name",
    header: "Name",
  },
  {
    accessorKey: "typeName",
    header: "Type",
  },
  {
    accessorKey: "description",
    header: "Description",
  },
  {
    accessorKey: "status",
    header: "Status",
  },
];
```

The `"use client"` directive is required because this file will be imported by the DataTable client component. The `ColumnDef` generic takes your data type so that `accessorKey` is type-checked — if you misspell a property name, TypeScript catches it.

This is the bare minimum. Every column renders its value as plain text. We'll add custom renderers, sorting, and formatting later, but the table will work with just this.

## Step 3: The DataTable Component

This is the core file. It creates a TanStack table instance with `useReactTable` and renders it using shadcn's `<Table>` components. Here's the minimal version — no sorting, no filtering, no pagination. Just data in, table out.

```tsx
// File: src/components/cost-objects/CostObjectsDataTable.tsx
"use client";

import {
  ColumnDef,
  flexRender,
  getCoreRowModel,
  useReactTable,
} from "@tanstack/react-table";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";

interface DataTableProps<TData, TValue> {
  columns: ColumnDef<TData, TValue>[];
  data: TData[];
}

export function CostObjectsDataTable<TData, TValue>({
  columns,
  data,
}: DataTableProps<TData, TValue>) {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });

  return (
    <div className="overflow-hidden rounded-md border">
      <Table>
        <TableHeader>
          {table.getHeaderGroups().map((headerGroup) => (
            <TableRow key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <TableHead key={header.id}>
                  {header.isPlaceholder
                    ? null
                    : flexRender(
                        header.column.columnDef.header,
                        header.getContext(),
                      )}
                </TableHead>
              ))}
            </TableRow>
          ))}
        </TableHeader>
        <TableBody>
          {table.getRowModel().rows?.length ? (
            table.getRowModel().rows.map((row) => (
              <TableRow key={row.id}>
                {row.getVisibleCells().map((cell) => (
                  <TableCell key={cell.id}>
                    {flexRender(
                      cell.column.columnDef.cell,
                      cell.getContext(),
                    )}
                  </TableCell>
                ))}
              </TableRow>
            ))
          ) : (
            <TableRow>
              <TableCell
                colSpan={columns.length}
                className="h-24 text-center"
              >
                No results.
              </TableCell>
            </TableRow>
          )}
        </TableBody>
      </Table>
    </div>
  );
}
```

Let's unpack what's happening here. The `useReactTable` hook takes your data, columns, and a `getCoreRowModel` function. That last argument is required — it's the base model that computes which rows to display. Every other feature (sorting, filtering, pagination) layers on top of it as additional model functions.

The `flexRender` function is the bridge between TanStack's headless logic and your React JSX. A column's `header` or `cell` can be a string, a React component, or a render function. `flexRender` handles all three cases and returns the appropriate React element. You'll see it used twice — once for headers, once for cells — and you'll never need to think about it beyond that.

The generic `<TData, TValue>` on the component means this same component could technically render any data type. For now it's specific to cost objects by name, but the interface already accepts any `columns` and `data` pair.

## Step 4: The Server Page

The server page fetches data and passes it to the client components. This is where the serialization boundary lives.

```tsx
// File: src/app/(frontend)/cost-objects/page.tsx
import { columns } from "@/components/cost-objects/columns";
import { CostObjectsDataTable } from "@/components/cost-objects/CostObjectsDataTable";
import type { SerializedCostObject } from "@/components/cost-objects/types";

export default async function CostObjectsPage() {
  // Replace this with your actual data fetching
  const rawData = await fetchCostObjects();

  // Serialize into plain objects for the client
  const costObjects: SerializedCostObject[] = rawData.map((co) => ({
    id: co.id,
    code: co.code,
    name: co.name,
    typeName: typeof co.type === "object" ? co.type.name : null,
    description: co.description ?? null,
    status: co.status,
  }));

  return (
    <div className="space-y-6">
      <div>
        <h1 className="text-2xl font-bold tracking-tight">Cost Objects</h1>
        <p className="text-muted-foreground mt-1">
          Browse and filter all cost objects
        </p>
      </div>
      <CostObjectsDataTable columns={columns} data={costObjects} />
    </div>
  );
}
```

The `.map()` that produces `SerializedCostObject[]` is critical. If your data source returns rich objects with nested relationships (like `co.type` being a full object with its own `id`, `name`, and methods), you need to flatten them into plain values. React Server Components can only pass serializable data to client components. Passing a Payload CMS document directly, for example, would fail silently or throw a serialization error.

At this point you have a working table. It displays all your data in a clean bordered table with headers. No interactivity yet — but the foundation is solid. Every feature from here is additive.

## Step 5: Add Sorting

Sorting requires two changes: add a state and model function to the DataTable, and make column headers clickable in the columns file.

First, update the DataTable component:

```tsx
// File: src/components/cost-objects/CostObjectsDataTable.tsx
"use client";

import * as React from "react";
import {
  ColumnDef,
  SortingState,
  flexRender,
  getCoreRowModel,
  getSortedRowModel,
  useReactTable,
} from "@tanstack/react-table";
// ... Table imports remain the same

export function CostObjectsDataTable<TData, TValue>({
  columns,
  data,
}: DataTableProps<TData, TValue>) {
  const [sorting, setSorting] = React.useState<SortingState>([]);

  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    onSortingChange: setSorting,
    state: { sorting },
  });

  // ... return JSX remains identical
}
```

Three additions: a `SortingState` that tracks which column is sorted and in which direction, the `getSortedRowModel` function that tells TanStack to compute sorted rows, and the `onSortingChange` callback that keeps your state in sync. The render code doesn't change at all — TanStack's `table.getRowModel().rows` now returns sorted rows automatically.

Now make the column headers interactive. Update the columns you want to be sortable:

```tsx
// File: src/components/cost-objects/columns.tsx (updated)
"use client";

import { ColumnDef } from "@tanstack/react-table";
import { ArrowUpDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import type { SerializedCostObject } from "./types";

export const columns: ColumnDef<SerializedCostObject>[] = [
  {
    accessorKey: "code",
    header: ({ column }) => (
      <Button
        variant="ghost"
        onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
      >
        Code
        <ArrowUpDown className="ml-2 h-4 w-4" />
      </Button>
    ),
  },
  {
    accessorKey: "name",
    header: ({ column }) => (
      <Button
        variant="ghost"
        onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
      >
        Name
        <ArrowUpDown className="ml-2 h-4 w-4" />
      </Button>
    ),
  },
  {
    accessorKey: "typeName",
    header: "Type",
  },
  // ... other columns unchanged
];
```

The `header` property accepts either a string or a function. When it's a function, it receives the column instance which has methods like `toggleSorting` and `getIsSorted`. The `ArrowUpDown` icon from Lucide gives users a visual cue that the column is sortable. Columns that don't need sorting — like "Type" — keep the plain string header.

The pattern here is worth noting: you don't need to make every column sortable. Only add the interactive header to columns where sorting is useful. TanStack doesn't force uniformity.

## Step 6: Add Filtering

Filtering follows the same pattern as sorting — add state, add a model function, add UI. Update the DataTable:

```tsx
// File: src/components/cost-objects/CostObjectsDataTable.tsx (updated)
import {
  ColumnDef,
  SortingState,
  ColumnFiltersState,
  flexRender,
  getCoreRowModel,
  getSortedRowModel,
  getFilteredRowModel,
  useReactTable,
} from "@tanstack/react-table";
import { Input } from "@/components/ui/input";

export function CostObjectsDataTable<TData, TValue>({
  columns,
  data,
}: DataTableProps<TData, TValue>) {
  const [sorting, setSorting] = React.useState<SortingState>([]);
  const [columnFilters, setColumnFilters] =
    React.useState<ColumnFiltersState>([]);

  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    state: { sorting, columnFilters },
  });

  return (
    <div className="space-y-4">
      <Input
        placeholder="Filter by name..."
        value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
        onChange={(e) =>
          table.getColumn("name")?.setFilterValue(e.target.value)
        }
        className="max-w-sm"
      />
      <div className="overflow-hidden rounded-md border">
        <Table>
          {/* ... same table rendering as before */}
        </Table>
      </div>
    </div>
  );
}
```

The `getFilteredRowModel` tells TanStack to compute filtered rows. The `ColumnFiltersState` is an array of `{ id, value }` pairs — one entry per filtered column. When the user types in the input, `setFilterValue` on the "name" column updates that state, and TanStack recomputes the visible rows.

The filter input targets a specific column by name via `table.getColumn("name")`. You can add multiple filter inputs for different columns using the same pattern. Each column filters independently, and TanStack applies them all before passing rows to the sort and pagination models.

One thing to watch: the string you pass to `table.getColumn()` must match the `accessorKey` in your column definition. If your column uses `accessorKey: "typeName"`, the filter must target `"typeName"`, not `"type"` or `"Type"`.

## Step 7: Add Pagination

Pagination is the simplest feature to add because it requires no changes to the column definitions.

```tsx
// File: src/components/cost-objects/CostObjectsDataTable.tsx (updated)
import {
  // ... existing imports
  getPaginationRowModel,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";

export function CostObjectsDataTable<TData, TValue>({
  columns,
  data,
}: DataTableProps<TData, TValue>) {
  const [sorting, setSorting] = React.useState<SortingState>([]);
  const [columnFilters, setColumnFilters] =
    React.useState<ColumnFiltersState>([]);

  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    state: { sorting, columnFilters },
    initialState: {
      pagination: { pageSize: 25 },
    },
  });

  return (
    <div className="space-y-4">
      <Input
        placeholder="Filter by name..."
        value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
        onChange={(e) =>
          table.getColumn("name")?.setFilterValue(e.target.value)
        }
        className="max-w-sm"
      />
      <div className="overflow-hidden rounded-md border">
        <Table>
          {/* ... same table rendering */}
        </Table>
      </div>
      <div className="flex items-center justify-between">
        <p className="text-sm text-muted-foreground">
          {table.getFilteredRowModel().rows.length} result(s) total
        </p>
        <div className="flex items-center gap-2">
          <Button
            variant="outline"
            size="sm"
            onClick={() => table.previousPage()}
            disabled={!table.getCanPreviousPage()}
          >
            Previous
          </Button>
          <span className="text-sm text-muted-foreground">
            Page {table.getState().pagination.pageIndex + 1} of{" "}
            {table.getPageCount()}
          </span>
          <Button
            variant="outline"
            size="sm"
            onClick={() => table.nextPage()}
            disabled={!table.getCanNextPage()}
          >
            Next
          </Button>
        </div>
      </div>
    </div>
  );
}
```

Adding `getPaginationRowModel()` is enough to enable pagination. TanStack defaults to 10 rows per page. I override that with `initialState: { pagination: { pageSize: 25 } }` because 10 feels too few for a data-heavy table.

The pagination API is straightforward: `table.previousPage()` and `table.nextPage()` navigate, `table.getCanPreviousPage()` and `table.getCanNextPage()` tell you when to disable buttons, and `table.getPageCount()` gives you the total.

One important behavior: when the user applies a filter that reduces the row count below the current page, TanStack automatically resets to page one. You don't need to handle that edge case manually. This is exactly the kind of state coordination that makes the library worth using.

## Step 8: Custom Cell Renderers

So far, every cell renders its value as plain text. Real tables need links, badges, formatted dates, and truncated descriptions. You control this through the `cell` property on each column definition.

Here's the complete columns file with custom renderers:

```tsx
// File: src/components/cost-objects/columns.tsx (final)
"use client";

import { ColumnDef } from "@tanstack/react-table";
import { ArrowUpDown } from "lucide-react";
import Link from "next/link";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import type { SerializedCostObject } from "./types";

export const columns: ColumnDef<SerializedCostObject>[] = [
  {
    accessorKey: "code",
    header: ({ column }) => (
      <Button
        variant="ghost"
        onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
      >
        Code
        <ArrowUpDown className="ml-2 h-4 w-4" />
      </Button>
    ),
    cell: ({ row }) => (
      <Link
        href={`/cost-objects/${encodeURIComponent(row.original.code)}`}
        className="font-mono text-sm font-medium hover:underline underline-offset-2"
      >
        {row.original.code}
      </Link>
    ),
  },
  {
    accessorKey: "name",
    header: ({ column }) => (
      <Button
        variant="ghost"
        onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
      >
        Name
        <ArrowUpDown className="ml-2 h-4 w-4" />
      </Button>
    ),
  },
  {
    accessorKey: "typeName",
    header: "Type",
    cell: ({ row }) => (
      <span className="text-muted-foreground text-sm">
        {row.getValue("typeName") || "\u2014"}
      </span>
    ),
  },
  {
    accessorKey: "description",
    header: "Description",
    cell: ({ row }) => (
      <span className="text-muted-foreground text-sm max-w-[300px] truncate block">
        {row.getValue("description") || "\u2014"}
      </span>
    ),
  },
  {
    accessorKey: "status",
    header: "Status",
    cell: ({ row }) => {
      const status = row.getValue("status") as string;
      return (
        <Badge variant={status === "active" ? "default" : "secondary"}>
          {status === "active" ? "Active" : "Archived"}
        </Badge>
      );
    },
  },
];
```

There are two ways to access data inside a `cell` function. `row.getValue("columnId")` returns the value for a specific column through TanStack's accessor system. `row.original` returns the entire original data object. Use `row.original` when you need multiple fields — like building a link URL from `code` while displaying `code` as text. Use `row.getValue()` when you only need the column's own value.

The em dash (`\u2014`) for null values is a small detail that makes a difference. An empty cell looks like a rendering bug. A dash communicates "no value" intentionally.

For the description column, `max-w-[300px] truncate block` prevents long text from blowing out the table layout. The `block` class is necessary because `truncate` requires a block-level element to work — by default, a `<span>` is inline and won't clip.

## Step 9: Extract a Toolbar

As your table grows, the filter input and any other controls above the table become a toolbar. Extracting it into its own component keeps the DataTable focused on rendering.

```tsx
// File: src/components/cost-objects/CostObjectsDataTableToolbar.tsx
"use client";

import { Table } from "@tanstack/react-table";
import { Input } from "@/components/ui/input";

interface ToolbarProps<TData> {
  table: Table<TData>;
}

export function CostObjectsDataTableToolbar<TData>({
  table,
}: ToolbarProps<TData>) {
  return (
    <div className="flex items-center gap-2">
      <Input
        placeholder="Filter by name..."
        value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
        onChange={(e) =>
          table.getColumn("name")?.setFilterValue(e.target.value)
        }
        className="max-w-sm"
      />
      <Input
        placeholder="Filter by code..."
        value={(table.getColumn("code")?.getFilterValue() as string) ?? ""}
        onChange={(e) =>
          table.getColumn("code")?.setFilterValue(e.target.value)
        }
        className="max-w-xs"
      />
    </div>
  );
}
```

The toolbar receives the `table` instance and uses the same `getColumn().setFilterValue()` API. You can add as many filter inputs, dropdowns, or action buttons as needed without touching the DataTable component.

Update the DataTable to use the toolbar:

```tsx
// File: src/components/cost-objects/CostObjectsDataTable.tsx (with toolbar)
import { CostObjectsDataTableToolbar } from "./CostObjectsDataTableToolbar";

export function CostObjectsDataTable<TData, TValue>({
  columns,
  data,
}: DataTableProps<TData, TValue>) {
  // ... useReactTable setup unchanged

  return (
    <div className="space-y-4">
      <CostObjectsDataTableToolbar table={table} />
      <div className="overflow-hidden rounded-md border">
        <Table>
          {/* ... table rendering */}
        </Table>
      </div>
      {/* ... pagination controls */}
    </div>
  );
}
```

This is cleaner. The DataTable handles structure and rendering. The toolbar handles user controls. Each has a single responsibility.

## Gotchas Worth Knowing

There are a few things that tripped me up during implementation that aren't obvious from the documentation.

The `accessorKey` must match your data property exactly. If your serialized type has `typeName` but you write `accessorKey: "type_name"`, the column will render empty with no error. TypeScript catches this if you type your `ColumnDef` correctly, but it's easy to miss when renaming fields.

The `"use client"` directive is required on both `columns.tsx` and the DataTable component. If you forget it on the columns file and it only exports a plain array, you might not get an error immediately — but the moment a column uses JSX in its `header` or `cell`, the build will fail with a confusing message about server components not supporting functions.

When you pass data from a server component to the DataTable, every value must be serializable. Dates should be ISO strings, not `Date` objects. Nested relationships should be flattened. If you pass a `Date` object, it might work in development but fail in production builds.

The order of model functions matters conceptually but not mechanically. TanStack applies them in a fixed pipeline: core rows, then filtered, then sorted, then paginated. The order you list them in the `useReactTable` config doesn't change the behavior, but listing them in pipeline order makes the code easier to read.

Filter values are always per-column by default. TanStack doesn't include a built-in global search across all columns. If you need global filtering, you can add a custom `filterFn` on individual columns or use the `globalFilter` state, which is a separate API from `columnFilters`.

## Conclusion

Building a Data Table with TanStack Table follows a consistent pattern: define your columns, create a table instance with the features you need, and render with shadcn's `<Table>`. Each feature — sorting, filtering, pagination — is a state variable plus a model function. The render code barely changes as you add capabilities.

The three-file architecture keeps concerns separated across the server/client boundary: the page fetches and serializes data, the columns file defines structure and appearance, and the DataTable component handles rendering and state. Once this skeleton is in place, adding a new column or a new feature is a localized change rather than a refactor.

What makes this approach scale is that TanStack coordinates the interaction between features for you. Filtering resets pagination. Sorting preserves the current filter. You declare what you want, and the library handles how they compose.

Let me know in the comments if you have questions, and subscribe for more practical development guides.

Thanks, Matija