Construyendo Tablas Dinámicas en Next.js con Shadcn y TanStack

Published on
Construyendo Tablas Dinámicas en Next.js con Shadcn y TanStack

¿Estás usando nextjs, shadcn y necesitas una tabla más robusta para mostrar tu información?

Puedes combinar los componentes de interfaz de usuario de Shadcn con TanStack Table en Next.js para crear tablas de datos dinámicas e interactivas. Esta integración aprovecha el diseño minimalista de Shadcn y las poderosas características de TanStack como la ordenación, el filtrado y la paginación.

Documentación de TanStack Table: TanStack TableExternal Link

Documentación de shadcn: shadcnExternal Link

Aquí tienes un ejemplo de la tabla que vamos a construir:

shadcn table example

Si quieres ver el código completo: Shadcn Table ExampleExternal Link

1. Instalación

Shadcn tiene una excelente guía de instalación en caso de que quieras consultarla: Guía de instalaciónExternal Link

Si no has instalado shadcn, necesitarás comenzar con npx shadcn-ui@latest init, luego necesitaremos instalar algunos otros componentes de shadcn:

1. Componentes de Shadcn

npx shadcn-ui@latest add table badge button command popover separator dropdown-menu input select
bash

2. TanStack Table

npm install @tanstack/react-table
bash

3. Instalaciones Opcionales

Iconos para hacer la información más atractiva Radix iconsExternal Link

npm install @radix-ui/react-icons
bash

Y en caso de que estés usando zod para la declaración y validación de esquemas.

Documentación de Zod: ZodExternal Link

npm install zod
bash

2. Data y Estructura

Necesitaremos crear un directorio bajo app/users y agregar los siguientes archivos:

app
└── users
    ├── columns.tsx
    ├── data-table-column-header.tsx
    ├── data-table-faceted-filter.tsx
    ├── data-table-pagination.tsx
    ├── data-table-row-actions.tsx
    ├── data-table-toolbar.tsx
    ├── data-table-view-options.tsx
    ├── data-table.tsx
    ├── definitions.ts
    ├── users.ts
    ├── userSchema.ts
└── page.tsx
types.d.ts
Estructura de carpetas
  • columns.tsx (componente del cliente) contendrá nuestras definiciones de columna.
  • users.ts (datos locales de usuarios).
  • data-table-column-header.tsx (componente de cliente) contendrá la ordenación del encabezado.
  • data-table-faceted-filter.tsx (componente de cliente) ordenación y filtrado por estado.
  • data-table-pagination.tsx (componente de cliente) manejará la paginación de los datos.
  • data-table-row-actions.tsx (componente de cliente) opciones de la tabla (editar datos, enlazar a otras páginas).
  • data-table-toolbar.tsx (componente de cliente) filtrado de los datos de la tabla.
  • data-table-view-options.tsx (componente de cliente) ocultar y mostrar columnas.
  • data-table.tsx (componente de cliente) contendrá nuestro componente <DataTable />.
  • definitions.ts (definiciones de datos de estado y rol)
  • page.tsx (componente de servidor) es donde obtendremos los datos y renderizaremos nuestra tabla.

Estaremos usando información de usuario como datos locales. Así es como se verán los datos:

export const users: User[] = [
  {
    id: '9953ed85-31a0-4db9-acc8-e25b76176443',
    userName: 'John Miller',
    phone: '+1-555-0101',
    email: 'john.miller@example.com',
    role: 'client',
    status: 'inactive',
    location: '4306 Highland Drive, Seattle, WA 98109',
    image: 'john.miller.jpg',
    rtn: 'US2347908701',
    otherInformation: 'John Miller works in a tech startup in Seattle.',
  },
  // ...
]
app/users/users.ts

Puedes crear un archivo de tipos en la raíz de tu proyecto:

type User = {
  id: string;
  userName: string;
  phone: string;
  email: string;
  location: string;
  role: 'client' | 'provider';
  status: 'active' | 'inactive';
  image: string;
  rtn?: string;
  otherInformation?: string;
  createdAt?: date;
  updatedAt?: date;
};
types.d.ts

Podemos definir las columnas que necesitamos y qué datos se mostrarán, formatearán, ordenarán y filtrarán.

Comencemos por crear las definiciones de las columnas:

"use client";

import { ColumnDef } from "@tanstack/react-table";
import clsx from "clsx";

import { DataTableColumnHeader } from "@/app/users/data-table-column-header";
import { DataTableRowActions } from "@/app/users/data-table-row-actions";
import { usersRole, usersStatus } from "@/app/users/definitions";

export const columns: ColumnDef<User>[] = [
  {
    accessorKey: "userName",
    header: ({ column }) => <DataTableColumnHeader column={column} title={"User"} />,
    cell: ({ row }) => {
      return <div className="font-medium">{row.getValue("userName")}</div>;
    },
  },
  {
    accessorKey: "phone",
    header: ({ column }) => <DataTableColumnHeader column={column} title={"Phone"} />,
  },
  {
    accessorKey: "email",
    header: ({ column }) => <DataTableColumnHeader column={column} title={"Email"} />,
  },
  {
    accessorKey: "location",
    header: ({ column }) => <DataTableColumnHeader column={column} title={"Location"} />,
  },
  {
    accessorKey: "role",
    header: ({ column }) => <DataTableColumnHeader column={column} title={"Role"} />,
    cell: ({ row }) => {
      const role = usersRole.find((role) => role.value === row.getValue("role"));

      if (!role) {
        // Si un valor no es el esperado o no existe, puedes devolver nulo.
        return null;
      }

      return <span>{role.label}</span>;
    },
  },
  {
    accessorKey: "rtn",
    header: ({ column }) => <DataTableColumnHeader column={column} title={"RTN"} />,
  },
  {
    accessorKey: "otherInformation",
    header: ({ column }) => <DataTableColumnHeader column={column} title={"Other Info"} />,
  },
  {
    accessorKey: "status",
    header: ({ column }) => <DataTableColumnHeader column={column} title={"Status"} />,
    cell: ({ row }) => {
      const status = usersStatus.find((status) => status.value === row.getValue("status"));

      if (!status) {
        return null;
      }

      return (
        <div
          className={clsx("flex w-[100px] items-center", {
            "text-red-500": status.value === "inactive",
            "text-green-500": status.value === "active",
          })}>
          {status.icon && <status.icon className="mr-2 h-4 w-4 text-muted-foreground" />}
          <span>{status.label}</span>
        </div>
      );
    },
    filterFn: (row, id, value) => {
      return value.includes(row.getValue(id));
    },
  },
  {
    id: "actions",
    cell: ({ row }) => <DataTableRowActions row={row} />,
  },
];
app/users/columns.tsx

Como podemos observar, todas las columnas tienen un accessorKey y es la misma que los campos del tipo User definidos anteriormente.

El header y la cell pueden modificarse para mostrar una interfaz específica según tus necesidades; por ejemplo, el estado mostrará texto verde si el estado es activo y texto rojo si es inactivo.

Si no estás seguro de si algunos valores en tus datos estarán disponibles o no, puedes simplemente devolver null justo como en la linea 36.

Los otros archivos se verán así:

Las definiciones contienen información de lo que será filtrado, texto e iconos para la interfaz.

import { CheckCircledIcon, CrossCircledIcon } from "@radix-ui/react-icons";

export const usersStatus = [
  {
    value: "active",
    label: "Active",
    icon: CheckCircledIcon,
  },
  {
    value: "inactive",
    label: "Inactive",
    icon: CrossCircledIcon,
  },
];

export const usersRole = [
  {
    value: "client",
    label: "Client",
  },
  {
    value: "provider",
    label: "Provider",
  },
];
app/users/definitions.ts

Este archivo contiene los encabezados y también añade la funcionalidad de ordenación dependiendo de la columna.

import { ArrowDownIcon, ArrowUpIcon, CaretSortIcon, EyeNoneIcon } from "@radix-ui/react-icons";
import { Column } from "@tanstack/react-table";

import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

interface DataTableColumnHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
  column: Column<TData, TValue>;
  title: string;
}

export function DataTableColumnHeader<TData, TValue>({
  column,
  title,
  className,
}: DataTableColumnHeaderProps<TData, TValue>) {
  if (!column.getCanSort()) {
    return <div className={cn(className)}>{title}</div>;
  }

  return (
    <div className={cn("flex items-center space-x-2", className)}>
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <Button variant="ghost" size="sm" className="-ml-3 h-8 data-[state=open]:bg-accent">
            <span>{title}</span>
            {column.getIsSorted() === "desc" ? (
              <ArrowDownIcon className="ml-2 h-4 w-4" />
            ) : column.getIsSorted() === "asc" ? (
              <ArrowUpIcon className="ml-2 h-4 w-4" />
            ) : (
              <CaretSortIcon className="ml-2 h-4 w-4" />
            )}
          </Button>
        </DropdownMenuTrigger>
        <DropdownMenuContent align="start">
          <DropdownMenuItem onClick={() => column.toggleSorting(false)}>
            <ArrowUpIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
            {"Asc"}
          </DropdownMenuItem>
          <DropdownMenuItem onClick={() => column.toggleSorting(true)}>
            <ArrowDownIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
            {"Desc"}
          </DropdownMenuItem>
          <DropdownMenuSeparator />
          <DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
            <EyeNoneIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
            {"Hide"}
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
  );
}
app/users/data-table-column-header.tsx

Podemos añadir acciones como ver, editar y eliminar información de una fila en particular.

"use client";

// import { UserSchema } from "@/app/users/userSchema";
import Link from "next/link";
import { DotsHorizontalIcon } from "@radix-ui/react-icons";
import { Row } from "@tanstack/react-table";
import { Eye, Pencil, Trash2 } from "lucide-react";

import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

interface DataTableRowActionsProps<TData> {
  row: Row<TData>;
}

export function DataTableRowActions<TData>({ row }: DataTableRowActionsProps<TData>) {
  // const user = UserSchema.parse(row.original);
  // console.log(user.id); // Nota: use el id para cualquier acción (ejemplo: eliminar, ver, editar)

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="ghost" className="flex h-8 w-8 p-0 data-[state=open]:bg-muted">
          <DotsHorizontalIcon className="h-4 w-4" />
          <span className="sr-only">{"Open Menu"}</span>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem>
          <Button variant={"ghost"} size={"sm"} className={"justify-start w-full"} asChild>
            <Link href={"#"}>
              <Eye className="w-4 h-4 text-blue-500" />
              {<span className="ml-2">{"View"}</span>}
            </Link>
          </Button>
        </DropdownMenuItem>

        <DropdownMenuItem>
          <Button variant={"ghost"} size={"sm"} className={"justify-start w-full"} asChild>
            <Link href={"#"}>
              <Pencil className="h-4 w-4 text-green-500" />
              {<span className="ml-2">{"Update"}</span>}
            </Link>
          </Button>
        </DropdownMenuItem>

        <DropdownMenuItem>
          <Button variant={"ghost"} size={"sm"} className={"justify-start w-full"}>
            <Trash2 className="h-4 w-4 text-red-500" />
            {<span className="ml-2">{"Delete"}</span>}
          </Button>
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
app/users/data-table-row-actions.tsx

En caso de que decidas usar zod para la validación de tus datos, así es como podemos crear un esquema simple.

import { z } from "zod";

export enum Role {
  "provider" = "provider",
  "client" = "client",
}
export enum UserStatus {
  "active" = "active",
  "inactive" = "inactive",
}

export const UserSchema = z.object({
  id: z.string(),

  userName: z.string({}).trim().min(5),

  phone: z.string({}).trim().min(8),

  email: z.string({}).email().trim().or(z.literal("")).optional(),

  location: z.string({}).trim().or(z.literal("")).optional(),

  role: z.nativeEnum(Role, {}),

  status: z.nativeEnum(UserStatus, {}),

  otherInformation: z.string({}).trim().or(z.literal("")).optional(),

  rtn: z.string({}).trim().or(z.literal("")).optional(),

  image: z.string().or(z.literal("")).optional(),

  createdAt: z.date().optional(),
  updatedAt: z.date().optional(),
});
app/users/userSchema.ts

A continuación, crearemos un componente <DataTable /> para ayudarnos a renderizar nuestra tabla. Este archivo contendrá <DataTablePagination /> y <DataTableToolbar />.

"use client";

import React from "react";
import {
  ColumnDef,
  ColumnFiltersState,
  flexRender,
  getCoreRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  SortingState,
  useReactTable,
  VisibilityState,
} from "@tanstack/react-table";

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";

import { DataTablePagination } from "./data-table-pagination";
import { DataTableToolbar } from "./data-table-toolbar";

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

export default function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
  const [sorting, setSorting] = React.useState<SortingState>([]);
  const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({
    location: false,
    otherInformation: false,
  });
  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);

  const table = useReactTable({
    data,
    columns,
    state: {
      sorting,
      columnVisibility,
      columnFilters,
    },
    enableRowSelection: true,
    getCoreRowModel: getCoreRowModel(),
    onSortingChange: setSorting,
    getSortedRowModel: getSortedRowModel(),
    onColumnVisibilityChange: setColumnVisibility,
    getFilteredRowModel: getFilteredRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    onColumnFiltersChange: setColumnFilters,
  });

  return (
    <div className="space-y-4">
      <DataTableToolbar table={table} />
      <div className="rounded-md border">
        <Table>
          <TableHeader>
            {table.getHeaderGroups().map((headerGroup) => (
              <TableRow key={headerGroup.id}>
                {headerGroup.headers.map((header) => {
                  return (
                    <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} data-state={row.getIsSelected() && "selected"}>
                  {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 data results"}
                </TableCell>
              </TableRow>
            )}
          </TableBody>
        </Table>
      </div>
      <DataTablePagination table={table} />
    </div>
  );
}
app/users/data-table.tsx

La paginación de la tabla de datos tendrá botones para moverse a través de las páginas de datos.

import {
  ChevronLeftIcon,
  ChevronRightIcon,
  DoubleArrowLeftIcon,
  DoubleArrowRightIcon,
} from "@radix-ui/react-icons";
import { Table } from "@tanstack/react-table";

import { Button } from "@/components/ui/button";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";

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

export function DataTablePagination<TData>({ table }: DataTablePaginationProps<TData>) {
  return (
    <div className="flex items-center justify-between px-2">
      <div className="flex-1 text-sm text-muted-foreground">
        {table.getFilteredSelectedRowModel().rows.length} of{" "}
        {table.getFilteredRowModel().rows.length} {"row(s) selected"}.
      </div>
      <div className="flex items-center space-x-6 lg:space-x-8">
        <div className="flex items-center space-x-2">
          <p className="text-sm font-medium">{"Rows per page"}</p>
          <Select
            value={`${table.getState().pagination.pageSize}`}
            onValueChange={(value) => {
              table.setPageSize(Number(value));
            }}>
            <SelectTrigger className="h-8 w-[70px]">
              <SelectValue placeholder={table.getState().pagination.pageSize} />
            </SelectTrigger>
            <SelectContent side="top">
              {[10, 20, 30, 40, 50].map((pageSize) => (
                <SelectItem key={pageSize} value={`${pageSize}`}>
                  {pageSize}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
        </div>
        <div className="flex w-[100px] items-center justify-center text-sm font-medium">
          {"Page"} {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
        </div>
        <div className="flex items-center space-x-2">
          <Button
            variant="outline"
            className="hidden h-8 w-8 p-0 lg:flex"
            onClick={() => table.setPageIndex(0)}
            disabled={!table.getCanPreviousPage()}>
            <span className="sr-only">{"Go to first page"}</span>
            <DoubleArrowLeftIcon className="h-4 w-4" />
          </Button>
          <Button
            variant="outline"
            className="h-8 w-8 p-0"
            onClick={() => table.previousPage()}
            disabled={!table.getCanPreviousPage()}>
            <span className="sr-only">{"Go to previous page"}</span>
            <ChevronLeftIcon className="h-4 w-4" />
          </Button>
          <Button
            variant="outline"
            className="h-8 w-8 p-0"
            onClick={() => table.nextPage()}
            disabled={!table.getCanNextPage()}>
            <span className="sr-only">{"Go to next page"}</span>
            <ChevronRightIcon className="h-4 w-4" />
          </Button>
          <Button
            variant="outline"
            className="hidden h-8 w-8 p-0 lg:flex"
            onClick={() => table.setPageIndex(table.getPageCount() - 1)}
            disabled={!table.getCanNextPage()}>
            <span className="sr-only">{"Go to last page"}</span>
            <DoubleArrowRightIcon className="h-4 w-4" />
          </Button>
        </div>
      </div>
    </div>
  );
}
app/users/data-table-pagination.tsx

La barra de herramientas de la tabla de datos contendrá las opciones de visualización (mostrar/ocultar columnas), ordenación y filtrado.

"use client";

import { Cross2Icon } from "@radix-ui/react-icons";
import { Table } from "@tanstack/react-table";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { usersStatus } from "@/app/users/definitions";

import { DataTableFacetedFilter } from "./data-table-faceted-filter";
import { DataTableViewOptions } from "./data-table-view-options";

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

export function DataTableToolbar<TData>({ table }: DataTableToolbarProps<TData>) {
  const isFiltered = table.getState().columnFilters.length > 0;

  return (
    <div className="flex w-full items-center justify-between">
      <div className="flex flex-1 items-center space-x-2">
        <Input
          placeholder={"Filter"}
          value={(table.getColumn("userName")?.getFilterValue() as string) ?? ""}
          onChange={(event) => table.getColumn("userName")?.setFilterValue(event.target.value)}
          className="h-8 w-[150px] lg:w-[250px]"
        />

        {table.getColumn("status") && (
          <DataTableFacetedFilter
            column={table.getColumn("status")}
            title={"Status"}
            options={usersStatus}
          />
        )}

        {isFiltered && (
          <Button
            variant="outline"
            onClick={() => table.resetColumnFilters()}
            className="h-8 px-2 lg:px-3">
            {"Clean Filters"}
            <Cross2Icon className="ml-2 h-4 w-4" />
          </Button>
        )}
      </div>
      <DataTableViewOptions table={table} />
    </div>
  );
}
app/users/data-table-toolbar.tsx

La tabla puede ocultar y mostrar columnas que elijamos. En nuestro ejemplo, Location y OtherInformation están ocultos por defecto.

"use client";

import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
import { MixerHorizontalIcon } from "@radix-ui/react-icons";
import { Table } from "@tanstack/react-table";

import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuLabel,
  DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";

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

export function DataTableViewOptions<TData>({ table }: DataTableViewOptionsProps<TData>) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="sm" className="ml-auto hidden h-8 lg:flex">
          <MixerHorizontalIcon className="mr-2 h-4 w-4" />
          {"See Columns"}
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end" className="w-[160px]">
        <DropdownMenuLabel>{"Edit Columns"}</DropdownMenuLabel>
        <DropdownMenuSeparator />
        {table
          .getAllColumns()
          .filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide())
          .map((column) => {
            return (
              <DropdownMenuCheckboxItem
                key={column.id}
                className="capitalize"
                checked={column.getIsVisible()}
                onCheckedChange={(value) => column.toggleVisibility(!!value)}>
                {column.id}
              </DropdownMenuCheckboxItem>
            );
          })}
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
app/users/data-table-view-options.tsx

También podemos filtrar basados en un campo, en nuestro ejemplo podemos filtrar por el estado Active e Inactive.

import * as React from "react";
import { CheckIcon, PlusCircledIcon } from "@radix-ui/react-icons";
import { Column } from "@tanstack/react-table";

import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
} from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";

interface DataTableFacetedFilterProps<TData, TValue> {
  column?: Column<TData, TValue>;
  title?: string;
  options: {
    label: string;
    value: string;
    icon?: React.ComponentType<{ className?: string }>;
  }[];
}

export function DataTableFacetedFilter<TData, TValue>({
  column,
  title,
  options,
}: DataTableFacetedFilterProps<TData, TValue>) {
  const facets = column?.getFacetedUniqueValues();
  const selectedValues = new Set(column?.getFilterValue() as string[]);

  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button variant="outline" size="sm" className="h-8 border-dashed">
          <PlusCircledIcon className="mr-2 h-4 w-4" />
          {title}
          {selectedValues?.size > 0 && (
            <>
              <Separator orientation="vertical" className="mx-2 h-4" />
              <Badge variant="secondary" className="rounded-sm px-1 font-normal lg:hidden">
                {selectedValues.size}
              </Badge>
              <div className="hidden space-x-1 lg:flex">
                {selectedValues.size > 2 ? (
                  <Badge variant="secondary" className="rounded-sm px-1 font-normal">
                    {selectedValues.size} {"Selected"}
                  </Badge>
                ) : (
                  options
                    .filter((option) => selectedValues.has(option.value))
                    .map((option) => (
                      <Badge
                        variant="secondary"
                        key={option.value}
                        className="rounded-sm px-1 font-normal">
                        {option.label}
                      </Badge>
                    ))
                )}
              </div>
            </>
          )}
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-[200px] p-0" align="start">
        <Command>
          <CommandInput placeholder={title} />
          <CommandList>
            <CommandEmpty>{"No Filter results"}</CommandEmpty>
            <CommandGroup>
              {options.map((option) => {
                const isSelected = selectedValues.has(option.value);
                return (
                  <CommandItem
                    key={option.value}
                    onSelect={() => {
                      if (isSelected) {
                        selectedValues.delete(option.value);
                      } else {
                        selectedValues.add(option.value);
                      }
                      const filterValues = Array.from(selectedValues);
                      column?.setFilterValue(filterValues.length ? filterValues : undefined);
                    }}>
                    <div
                      className={cn(
                        "mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
                        isSelected
                          ? "bg-primary text-primary-foreground"
                          : "opacity-50 [&_svg]:invisible"
                      )}>
                      <CheckIcon className={cn("h-4 w-4")} />
                    </div>
                    {option.icon && <option.icon className="mr-2 h-4 w-4 text-muted-foreground" />}
                    <span>{option.label}</span>
                    {facets?.get(option.value) && (
                      <span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
                        {facets.get(option.value)}
                      </span>
                    )}
                  </CommandItem>
                );
              })}
            </CommandGroup>
            {selectedValues.size > 0 && (
              <>
                <CommandSeparator />
                <CommandGroup>
                  <CommandItem
                    onSelect={() => column?.setFilterValue(undefined)}
                    className="justify-center text-center">
                    {"Clean Filters"}
                  </CommandItem>
                </CommandGroup>
              </>
            )}
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  );
}
app/users/data-table-faceted-filter.tsx

Finalmente, renderizaremos nuestra tabla en nuestro componente de página.

import { columns } from "@/app/users/columns";
import DataTable from "@/app/users/data-table";
import { users } from "@/app/users/users";

export default async function Home() {
  // Aquí es donde buscarías datos externos:
  // const exampleExternalData = await fetchExternalDataFunction();

  // In our example we use local data
  return (
    <div className="container p-2">
      <DataTable data={users} columns={columns} />
    </div>
  );
}
app/page.tsx