TinyKit Pro Docs

Next.js 16 Patterns

TinyKit Pro uses Next.js 16.0.10 with React 19. This guide documents the key patterns and best practices used in the codebase.

TinyKit Pro uses Next.js 16.0.10 with React 19. This guide documents the key patterns and best practices used in the codebase.

Promise-Based Params and SearchParams

Next.js 14+ changed the behavior of params and searchParams to be Promise-based. This affects all route handlers and page components.

Server Components (Pages)

// src/app/products/[productId]/page.tsx
export default async function ProductPage({
  params,
  searchParams
}: {
  params: Promise<{ productId: string }>;
  searchParams: Promise<{ tab?: string }>
}) {
  // Await the params and searchParams
  const { productId } = await params;
  const { tab } = await searchParams;

  return (
    <div>
      <h1>Product: {productId}</h1>
      <p>Current tab: {tab ?? 'overview'}</p>
    </div>
  );
}

Route Handlers

// src/app/api/items/[itemId]/route.ts
export async function GET(
  request: Request,
  { params }: { params: Promise<{ itemId: string }> },
) {
  const { itemId } = await params;

  // Fetch item data...
  return Response.json({ itemId });
}

Client Components

For client components, continue using the useParams() hook from next/navigation:

"use client";

import { useParams } from "next/navigation";

export default function ClientPage() {
  const params = useParams();
  const orgSlug = params.orgSlug as string;

  // Use params directly
  return <div>Organization: {orgSlug}</div>;
}

URL Query State with nuqs

TinyKit Pro uses nuqs for URL query state management instead of Next.js's useSearchParams. This provides type-safe, declarative query state.

Why nuqs?

  • Type-safe URL state
  • Automatic URL encoding/decoding
  • Proper history management
  • Cleaner URLs with clearOnDefault

Basic Usage

"use client";

import { useQueryState, parseAsStringEnum, parseAsInteger } from "nuqs";

// Simple string filter
const [filter, setFilter] = useQueryState(
  "filter",
  parseAsStringEnum(["all", "active", "completed"])
    .withDefault("all")
    .withOptions({
      history: "push",
      clearOnDefault: true,
    }),
);

// Usage
void setFilter("active"); // Updates URL to ?filter=active
void setFilter("all"); // Clears filter from URL (default)

// Pagination
const [page, setPage] = useQueryState(
  "page",
  parseAsInteger.withDefault(1).withOptions({
    history: "replace",
    clearOnDefault: true,
  }),
);

Reusable Query State Hooks

Create centralized hooks in src/lib/navigation/:

// src/lib/navigation/useQueryState.ts
import { useQueryState, parseAsStringEnum, parseAsInteger } from "nuqs";

export const useFilterState = () => {
  return useQueryState(
    "filter",
    parseAsStringEnum(["all", "active", "completed"])
      .withDefault("all")
      .withOptions({ history: "push", clearOnDefault: true }),
  );
};

export const usePaginationState = () => {
  return useQueryState(
    "page",
    parseAsInteger.withDefault(1).withOptions({
      history: "replace",
      clearOnDefault: true,
    }),
  );
};

Type-Safe Routing

TinyKit Pro uses a type-safe routing system with the defineRoute utility.

Defining Routes

// src/config/routes/types.ts
export type RouteSchemas = {
  orgDashboard: {
    params: { orgSlug: string };
  };
  orgMember: {
    params: { orgSlug: string; memberId: string };
  };
  orgSettings: {
    params: { orgSlug: string };
    search?: { tab?: string };
  };
};

// src/config/routes/org.ts
import { defineRoute } from "@/lib/routes";

export const orgRoutes = {
  dashboard: defineRoute<RouteSchemas["orgDashboard"]>("/orgs/[orgSlug]"),
  member: defineRoute<RouteSchemas["orgMember"]>(
    "/orgs/[orgSlug]/members/[memberId]",
  ),
  settings: defineRoute<RouteSchemas["orgSettings"]>(
    "/orgs/[orgSlug]/settings",
  ),
};

Using Type-Safe Routes

import { orgRoutes } from "@/config/routes";
import { useRouter } from "next/navigation";

function NavigationExample() {
  const router = useRouter();

  // Navigate with type-safe params
  const handleNavigate = () => {
    router.push(orgRoutes.dashboard({ orgSlug: "my-org" }));

    // With search params
    router.push(
      orgRoutes.settings({
        orgSlug: "my-org",
        search: { tab: "members" },
      }),
    );
  };
}

Turbopack

Next.js 16 uses Turbopack as the default development bundler. No configuration needed - it's enabled automatically with next dev.

Benefits

  • Faster cold starts
  • Faster hot module replacement (HMR)
  • Improved memory usage
  • Better error messages

Compatibility

TinyKit Pro is fully compatible with Turbopack. If you encounter issues:

  1. Clear .next directory: rm -rf .next
  2. Clear node_modules cache: bun install --force
  3. Check for unsupported webpack plugins (rare)

React 19 Features

TinyKit Pro uses React 19.2.3. Key features used:

Server Components

Most pages are server components by default. Use "use client" directive only when needed:

// Server Component (default) - can be async, access server resources
export default async function Page() {
  const data = await fetchData(); // Direct server-side fetch
  return <div>{data.content}</div>;
}

// Client Component - for interactivity
"use client";
export default function InteractivePage() {
  const [state, setState] = useState(false);
  return <button onClick={() => setState(!state)}>Toggle</button>;
}

Suspense Boundaries

Use Suspense for loading states with server components:

import { Suspense } from "react";

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<StatsSkeleton />}>
        <StatsSection />
      </Suspense>
      <Suspense fallback={<ChartSkeleton />}>
        <ChartsSection />
      </Suspense>
    </div>
  );
}

Parallel Data Fetching

For server components, fetch data in parallel:

export default async function Page() {
  // Parallel fetching - much faster than sequential
  const [users, stats, settings] = await Promise.all([
    fetchUsers(),
    fetchStats(),
    fetchSettings(),
  ]);

  return (
    <Dashboard users={users} stats={stats} settings={settings} />
  );
}

Route Groups

TinyKit Pro uses route groups for organization:

src/app/
├── (root)/           # Public landing pages (/, /waitlist)
├── auth/             # Authentication pages
├── home/             # User dashboard
├── admin/            # Admin interface
└── orgs/
    ├── (list)/       # Organization list page
    └── (workspace)/  # Organization workspace pages
        └── [orgSlug]/

Route Group Conventions

  • (name) - Groups routes without affecting URL
  • [param] - Dynamic route segment
  • @slot - Parallel routes (slots)
  • (.)folder - Intercepting routes

Common Patterns

Authentication Guard

import { Authenticated } from "convex/react";

export default function ProtectedPage() {
  return (
    <Authenticated>
      <ProtectedContent />
    </Authenticated>
  );
}

Loading and Error States

// loading.tsx - Automatic loading UI
export default function Loading() {
  return <PageSkeleton />;
}

// error.tsx - Error boundary
"use client";
export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

Migration from Next.js 15

If migrating from Next.js 15:

  1. Update params typing: Add Promise<> wrapper to all params/searchParams
  2. Add await: Ensure you await params before accessing
  3. Check useSearchParams: Replace with nuqs for new code
  4. Test Turbopack: Verify all pages work with Turbopack
  5. Update dependencies: Ensure React 19 compatibility

← Back to Technical Documentation

On this page

Ship your startup faster. In minutes.

Get TinyKit Pro