jlnstack
Examples

Cookies Integration

Persist filter state in cookies using @jlnstack/cookies

Cookies Integration

Use @jlnstack/cookies to persist filter state across sessions.

Setup

Make sure you have both packages installed:

pnpm add @jlnstack/filter @jlnstack/cookies

Create a cookie to store the filter tree (without IDs):

cookies.ts
import { createCookie } from "@jlnstack/cookies/next"
import { z } from "zod"

export const userFiltersCookie = createCookie(
  "user-filters",
  z.record(z.any()).optional()
)

Server Component

Load initial filters from the cookie:

page.tsx
import { userFiltersCookie } from "./cookies"
import { UsersTable } from "./users-table"

export default async function UsersPage() {
  const savedFilters = await userFiltersCookie.get()

  return <UsersTable initialFilter={savedFilters ?? undefined} />
}

Client Component

Sync filter changes to the cookie:

users-table.tsx
"use client"

import { isGroup, type GroupInput } from "@jlnstack/filter"
import { createFilterHooks, FilterProvider } from "@jlnstack/filter/react"
import { userFilters } from "./filters"

const hooks = createFilterHooks(userFilters)

interface Props {
  initialFilter?: GroupInput<typeof userFilters>
}

function toInput(group) {
  return {
    type: "group",
    operator: group.operator,
    filters: group.filters.map((expr) =>
      isGroup(expr)
        ? toInput(expr)
        : { type: "condition", field: expr.field, value: expr.value }
    ),
  }
}

export function UsersTable({ initialFilter }: Props) {
  const filter = hooks.useFilter({
    defaultFilter: initialFilter,
    onFilterChange: async (filterTree) => {
      await fetch("/api/save-filters", {
        method: "POST",
        body: JSON.stringify(toInput(filterTree)),
      })
    },
  })

  return (
    <FilterProvider {...filter}>
      <FilterToolbar />
      <DataTable />
    </FilterProvider>
  )
}

API Route

Create an API route to save filters:

app/api/save-filters/route.ts
import { userFiltersCookie } from "@/cookies"
import { NextResponse } from "next/server"

export async function POST(request: Request) {
  const filters = await request.json()
  await userFiltersCookie.set(filters)
  return NextResponse.json({ ok: true })
}

Alternative: Server Action

Use a Server Action instead of an API route:

actions.ts
"use server"

import type { GroupInput } from "@jlnstack/filter"
import { userFiltersCookie } from "./cookies"
import { userFilters } from "./filters"

export async function saveFilters(filters: GroupInput<typeof userFilters>) {
  await userFiltersCookie.set(filters)
}
users-table.tsx
"use client"

import { saveFilters } from "./actions"

export function UsersTable({ initialFilter }: Props) {
  // Reuse toInput from the previous example.
  const filter = hooks.useFilter({
    defaultFilter: initialFilter,
    onFilterChange: (filterTree) => saveFilters(toInput(filterTree)),
  })

  // ...
}

Benefits

  • Persistent preferences — Filters survive browser restarts
  • No URL clutter — Keep URLs clean while maintaining state
  • Server-side access — Read filters in Server Components
  • Type-safe — Full validation with Zod schemas

On this page