Skip to main content

Component Architecture

Zarna components follow consistent patterns based on shadcn/ui conventions and React best practices.

Basic Component Structure

shadcn/ui Pattern

All shadcn components follow this structure:
// components/ui/button.tsx
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

// 1. Define variants with CVA
const buttonVariants = cva(
  // Base classes applied to all variants
  "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
        secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline"
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
        icon: "h-10 w-10"
      }
    },
    defaultVariants: {
      variant: "default",
      size: "default"
    }
  }
)

// 2. Define props interface
export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
}

// 3. Export component with variants
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = "Button"

export { Button, buttonVariants }

Component Patterns

1. Composable Components

Break components into smaller, reusable pieces:
// ✅ Good - Composable
<Card>
  <CardHeader>
    <CardTitle>Company Details</CardTitle>
    <CardDescription>View and edit company information</CardDescription>
  </CardHeader>
  <CardContent>
    <CompanyForm />
  </CardContent>
  <CardFooter>
    <Button>Save Changes</Button>
  </CardFooter>
</Card>

// ❌ Bad - Monolithic
<CompanyCard
  title="Company Details"
  description="View and edit..."
  content={<CompanyForm />}
  footer={<Button>Save</Button>}
/>

2. Polymorphic Components

Use asChild for component flexibility:
import { Button } from "@/components/ui/button"
import Link from "react-router-dom"

// Render as link
<Button asChild>
  <Link to="/dashboard">Go to Dashboard</Link>
</Button>

// Render as button (default)
<Button onClick={handleClick}>Click Me</Button>

3. Forwarding Refs

Always forward refs for flexibility:
const MyComponent = React.forwardRef<HTMLDivElement, MyComponentProps>(
  (props, ref) => {
    return <div ref={ref} {...props} />
  }
)
MyComponent.displayName = "MyComponent"

4. Controlled vs Uncontrolled

Controlled (recommended for forms):
function SearchInput() {
  const [value, setValue] = useState("")

  return (
    <Input
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  )
}
Uncontrolled (for simple cases):
function SearchInput() {
  const inputRef = useRef<HTMLInputElement>(null)

  const handleSubmit = () => {
    console.log(inputRef.current?.value)
  }

  return <Input ref={inputRef} />
}

Creating Custom Components

Example: Custom Card Component

"use client" // Only if using hooks or browser APIs

import * as React from "react"
import { cn } from "@/lib/utils"
import { cva, type VariantProps } from "class-variance-authority"
import { Badge } from "@/components/ui/badge"

const companyCardVariants = cva(
  "rounded-lg border bg-card text-card-foreground shadow-sm transition-all hover:shadow-md",
  {
    variants: {
      status: {
        active: "border-green-500",
        inactive: "border-gray-300",
        archived: "border-red-500 opacity-75"
      }
    },
    defaultVariants: {
      status: "active"
    }
  }
)

interface CompanyCardProps
  extends React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof companyCardVariants> {
  company: {
    id: string
    name: string
    industry?: string
    revenue?: number
    status: "active" | "inactive" | "archived"
  }
}

export function CompanyCard({
  company,
  status,
  className,
  ...props
}: CompanyCardProps) {
  return (
    <div
      className={cn(companyCardVariants({ status }), className)}
      {...props}
    >
      <div className="p-6">
        <div className="flex items-start justify-between">
          <div>
            <h3 className="text-lg font-semibold">{company.name}</h3>
            {company.industry && (
              <p className="text-sm text-muted-foreground mt-1">
                {company.industry}
              </p>
            )}
          </div>
          <Badge variant={company.status === "active" ? "default" : "secondary"}>
            {company.status}
          </Badge>
        </div>

        {company.revenue && (
          <p className="mt-4 text-2xl font-bold">
            ${(company.revenue / 1000000).toFixed(1)}M
          </p>
        )}
      </div>
    </div>
  )
}

Common Component Patterns

Loading States

function DataTable({ data, isLoading }: DataTableProps) {
  if (isLoading) {
    return (
      <div className="flex items-center justify-center p-8">
        <Loader2 className="h-8 w-8 animate-spin" />
        <span className="ml-2">Loading...</span>
      </div>
    )
  }

  return <Table data={data} />
}

Error States

function DataFetcher() {
  const [error, setError] = useState<Error | null>(null)

  if (error) {
    return (
      <Alert variant="destructive">
        <AlertCircle className="h-4 w-4" />
        <AlertTitle>Error</AlertTitle>
        <AlertDescription>{error.message}</AlertDescription>
      </Alert>
    )
  }

  return <DataDisplay />
}

Empty States

function CompanyList({ companies }: CompanyListProps) {
  if (companies.length === 0) {
    return (
      <div className="flex flex-col items-center justify-center p-8 text-center">
        <Building2 className="h-12 w-12 text-muted-foreground mb-4" />
        <h3 className="text-lg font-semibold mb-2">No companies found</h3>
        <p className="text-sm text-muted-foreground mb-4">
          Get started by creating your first company.
        </p>
        <Button>
          <Plus className="mr-2 h-4 w-4" />
          Create Company
        </Button>
      </div>
    )
  }

  return <div>{/* Render companies */}</div>
}

Form Components

Using React Hook Form + Zod

import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"

const formSchema = z.object({
  name: z.string().min(2, "Name must be at least 2 characters"),
  email: z.string().email("Invalid email address"),
  website: z.string().url("Invalid URL").optional()
})

export function CompanyForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      name: "",
      email: "",
      website: ""
    }
  })

  function onSubmit(values: z.infer<typeof formSchema>) {
    console.log(values)
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Company Name</FormLabel>
              <FormControl>
                <Input placeholder="Acme Corp" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl>
                <Input type="email" placeholder="contact@acme.com" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <Button type="submit">Submit</Button>
      </form>
    </Form>
  )
}

Server vs Client Components

When to Use “use client”

Only add "use client" when you need:
  • React Hooks: useState, useEffect, useContext, etc.
  • Browser APIs: window, document, localStorage, etc.
  • Event Handlers: onClick, onChange, onSubmit, etc.
  • Third-party libraries: That use hooks or browser APIs
// ❌ Doesn't need "use client"
export function StaticCard({ title, description }: CardProps) {
  return (
    <div>
      <h2>{title}</h2>
      <p>{description}</p>
    </div>
  )
}

// ✅ Needs "use client"
"use client"

export function InteractiveCard({ title }: CardProps) {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <div onClick={() => setIsOpen(!isOpen)}>
      <h2>{title}</h2>
      {isOpen && <p>Details</p>}
    </div>
  )
}

Performance Patterns

Lazy Loading

import { lazy, Suspense } from "react"

const HeavyComponent = lazy(() => import("./HeavyComponent"))

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <HeavyComponent />
    </Suspense>
  )
}

Memoization

import { useMemo, useCallback } from "react"

function ExpensiveComponent({ data }: Props) {
  // Memoize expensive calculations
  const processedData = useMemo(() => {
    return data.map(item => /* expensive operation */)
  }, [data])

  // Memoize callbacks
  const handleClick = useCallback(() => {
    console.log("Clicked")
  }, [])

  return <div onClick={handleClick}>{/* render */}</div>
}

React.memo

import React from "react"

interface Props {
  title: string
  count: number
}

// Only re-renders when props change
export const MemoizedComponent = React.memo(function Component({ title, count }: Props) {
  return (
    <div>
      <h2>{title}</h2>
      <p>Count: {count}</p>
    </div>
  )
})

Accessibility Patterns

Keyboard Navigation

function MenuItem({ onClick }: MenuItemProps) {
  return (
    <button
      onClick={onClick}
      onKeyDown={(e) => {
        if (e.key === "Enter" || e.key === " ") {
          onClick()
        }
      }}
    >
      Menu Item
    </button>
  )
}

ARIA Labels

<Button aria-label="Close dialog">
  <X className="h-4 w-4" />
</Button>

<Input
  aria-label="Search companies"
  aria-describedby="search-help"
  placeholder="Search..."
/>
<p id="search-help" className="text-sm text-muted-foreground">
  Enter company name or industry
</p>

Focus Management

function Dialog({ isOpen, onClose }: DialogProps) {
  const closeButtonRef = useRef<HTMLButtonElement>(null)

  useEffect(() => {
    if (isOpen) {
      closeButtonRef.current?.focus()
    }
  }, [isOpen])

  return (
    <DialogPrimitive.Root open={isOpen} onOpenChange={onClose}>
      {/* Dialog content */}
      <Button ref={closeButtonRef} onClick={onClose}>
        Close
      </Button>
    </DialogPrimitive.Root>
  )
}

Best Practices

// ✅ Good
<div className={cn("base-class", className)} />

// ❌ Bad
<div className={`base-class ${className}`} />
Keep components focused and readable by extracting complex logic
// ✅ Good
interface Props {
  title: string
  onClick: () => void
}

// ❌ Bad
function Component(props: any) {}
// ✅ Good
<nav><ul><li><a href="...">Link</a></li></ul></nav>

// ❌ Bad
<div><div><div onClick={...}>Link</div></div></div>

Component Organization Template

// 1. Imports
import * as React from "react"
import { cn } from "@/lib/utils"
import { cva } from "class-variance-authority"

// 2. Types
interface ComponentProps {
  // Props definition
}

// 3. Constants/Variants
const componentVariants = cva(/* ... */)

// 4. Component
export function Component(props: ComponentProps) {
  // 4a. Hooks
  const [state, setState] = useState()
  const ref = useRef()

  // 4b. Derived state
  const computed = useMemo(() => {}, [])

  // 4c. Handlers
  const handleClick = useCallback(() => {}, [])

  // 4d. Effects
  useEffect(() => {}, [])

  // 4e. Render
  return <div />
}

// 5. Sub-components (if needed)
Component.Header = function Header() {}
Component.Footer = function Footer() {}

Next Steps