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:Copy
// 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:Copy
// ✅ 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
UseasChild for component flexibility:
Copy
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:Copy
const MyComponent = React.forwardRef<HTMLDivElement, MyComponentProps>(
(props, ref) => {
return <div ref={ref} {...props} />
}
)
MyComponent.displayName = "MyComponent"
4. Controlled vs Uncontrolled
Controlled (recommended for forms):Copy
function SearchInput() {
const [value, setValue] = useState("")
return (
<Input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
)
}
Copy
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
Copy
"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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
// ❌ 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
Copy
import { lazy, Suspense } from "react"
const HeavyComponent = lazy(() => import("./HeavyComponent"))
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
)
}
Memoization
Copy
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
Copy
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
Copy
function MenuItem({ onClick }: MenuItemProps) {
return (
<button
onClick={onClick}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
onClick()
}
}}
>
Menu Item
</button>
)
}
ARIA Labels
Copy
<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
Copy
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
Always use cn() for className merging
Always use cn() for className merging
Copy
// ✅ Good
<div className={cn("base-class", className)} />
// ❌ Bad
<div className={`base-class ${className}`} />
Extract complex JSX to sub-components
Extract complex JSX to sub-components
Keep components focused and readable by extracting complex logic
Type all props
Type all props
Copy
// ✅ Good
interface Props {
title: string
onClick: () => void
}
// ❌ Bad
function Component(props: any) {}
Use semantic HTML
Use semantic HTML
Copy
// ✅ Good
<nav><ul><li><a href="...">Link</a></li></ul></nav>
// ❌ Bad
<div><div><div onClick={...}>Link</div></div></div>
Component Organization Template
Copy
// 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() {}
