Skip to Content
Iris Saas Kit documentation is under construction.
TechnicalNext.js Frontend

Next.js Frontend Architecture

Documentation for frontend code organization, styling system, and state management patterns.

🏗️ Frontend Code Organization

App Router Structure

The project uses Next.js 15 App Router with route groups for logical organization:

app/ ├── (authenticated)/ # Protected routes requiring auth │ ├── (user-app)/ # User-facing application │ └── (dev-app)/ # Developer/admin interface ├── (public-docs)/ # Public documentation ├── (public-with-convex-auth-client)/ # Auth pages └── (public-with-convex-client)/ # Public pages with Convex

Page-Level Organization

Each route follows a consistent pattern for organizing components and logic:

w/[workspace-slug]/ ├── page.tsx # Route component ├── layout.tsx # Layout wrapper ├── _components/ # Page-specific components │ ├── AppSidebar/ │ │ ├── index.tsx │ │ ├── AppSidebarHeader.tsx │ │ └── AppSidebarFooter.tsx │ ├── AppTopBar.tsx │ └── InviteMembersDialog.tsx └── _hooks/ # Page-specific custom hooks ├── useCurrentUser.get.tsx ├── useCurrentWorkspace.get.tsx └── useWorkspaceMembers.remove.tsx

Custom Hooks Pattern

Custom hooks combine data fetching with UI components for better encapsulation:

// _hooks/useCurrentUser.get.tsx export default function useGetCurrentUser() { const { data: _user, isPending } = useConvexQuery( api.core.users.getCurrentUser, { includeDetail: ["picture"] } ); const UserAvatar = ({ className, fallbackClassName }) => ( <Avatar className={className}> <AvatarImage src={_user?.profileUrl ?? ""} /> <AvatarFallback className={fallbackClassName}> {_user?.name?.charAt(0)} </AvatarFallback> </Avatar> ); return { user: { ..._user, Avatar: UserAvatar }, isPending, }; }

Key Benefits:

  • Encapsulation: Logic and UI components together
  • Reusability: Easy to use across different pages
  • Type Safety: Full TypeScript integration
  • Naming Convention: use<Feature>.<operation>.tsx

Enhanced Data Fetching

Uses custom useConvexQuery hook built on convex-helpers for richer query status:

// hooks/useConvexQuery.tsx import { makeUseQueryWithStatus } from 'convex-helpers/react'; import { useQueries } from 'convex/react'; const useConvexQuery = makeUseQueryWithStatus(useQueries); export default useConvexQuery;

Provides enhanced status beyond standard useQuery:

  • isPending - Initial loading state
  • isLoading - Any loading state
  • error - Error information
  • isSuccess - Success state

🎨 Styling System

Tailwind CSS Setup

Modern Configuration:

  • Tailwind CSS 4 with inline theme configuration
  • Modern color space: oklch() for better color perception
  • CSS Variables: Dynamic theming support
  • Custom variants: Dark mode with @custom-variant
/* globals.css */ @import 'tailwindcss'; @import 'tw-animate-css'; @custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); /* ... more variables */ }

Theme Management

Two-layer theming system:

  1. CSS Variables for dynamic colors
  2. next-themes for system/light/dark switching
// app/layout.tsx <ThemeProvider attribute='class' defaultTheme='system' enableSystem disableTransitionOnChange > {children} </ThemeProvider>

Features:

  • System preference detection
  • Seamless transitions disabled for performance
  • Class-based theme switching
  • Consistent color tokens across components

Component Library

shadcn/ui with extensive customization:

components/ui/ ├── accordion.tsx # Collapsible content ├── alert-dialog.tsx # Modal confirmations ├── avatar.tsx # User profile images ├── button.tsx # Primary interaction ├── card.tsx # Content containers ├── command.tsx # Command palette ├── dialog.tsx # Modal dialogs ├── dropdown-menu.tsx # Context menus ├── input.tsx # Form inputs ├── sidebar.tsx # Navigation sidebar └── ...

Design System Tokens:

  • Colors: Primary, secondary, muted, destructive
  • Spacing: Consistent margin/padding scale
  • Typography: Geist Sans + Geist Mono fonts
  • Border Radius: --radius system (0.625rem base)

🗄️ Global State Management

Zustand Store Pattern

Organized by feature domains:

// store/dialog.store.ts interface DialogStore { todos: { create: DialogControls; }; command: DialogControls; // More dialog groups... } export const useDialogStore = create<DialogStore>(set => ({ todos: { create: { isOpen: false, withToast: false, onOpen: options => set(/* ... */), onClose: () => set(/* ... */), }, }, command: { isOpen: false, onOpen: () => set(/* ... */), onClose: () => set(/* ... */), }, }));

Usage Pattern:

// In components const { isOpen, onOpen, onClose } = useDialogStore(state => state.todos.create);

Provider Architecture

Layered provider setup:

// app/layout.tsx <ConvexAuthNextjsServerProvider> <html lang='en' suppressHydrationWarning> <body> <ThemeProvider>{children}</ThemeProvider> <Toaster /> </body> </html> </ConvexAuthNextjsServerProvider>

Two Convex providers for different needs:

// ConvexClientProvider.tsx // For authenticated queries export function ConvexAuthClientProvider({ children }) { return ( <ConvexAuthNextjsProvider client={convex}> {children} </ConvexAuthNextjsProvider> ); } // For public queries export function ConvexClientProvider({ children }) { return <ConvexProvider client={convex}>{children}</ConvexProvider>; }

🛠️ Development Patterns

Component Organization

Hierarchical structure:

  • Shared components: /components/ directory
  • Page-specific: _components/ folders
  • Complex components: Subfolder with index.tsx
  • UI primitives: components/ui/ from shadcn

File Naming Conventions

  • Components: PascalCase.tsx
  • Hooks: use<Feature>.<operation>.tsx
  • Pages: page.tsx, layout.tsx, loading.tsx
  • Route groups: (group-name)/ for logical organization

Import Organization

Consistent import order:

  1. React & external libraries
  2. Convex imports
  3. Internal components
  4. Custom hooks
  5. Types & validators
  6. Relative imports
// External import React from 'react'; import { Button } from '@/components/ui/button'; // Convex import { api } from '@/convex/_generated/api'; // Internal import useCurrentUser from './_hooks/useCurrentUser.get'; // Types import type { WorkspaceId } from '@/convex/_generated/dataModel';

🔄 Key Benefits

Scalable Architecture:

  • Route groups prevent URL pollution
  • Page-specific organization reduces coupling
  • Custom hooks encapsulate complex logic

Developer Experience:

  • Consistent file organization
  • Type-safe throughout
  • Hot reload optimization
  • Path aliases for clean imports

Performance:

  • Server components by default
  • Optimized bundle splitting
  • Efficient re-renders with proper state isolation
Last updated on