โ† Back to DevBytes

State Management in Shadcn UI: Patterns and Libraries

Introduction to State Management in Shadcn UI

Shadcn UI has become one of the most popular component libraries in the React ecosystem, but unlike traditional component libraries, it doesn't ship as an npm package. Instead, Shadcn UI provides you with the raw source code of components that you copy directly into your project. This unique approach gives you complete control over styling, behavior, and โ€” crucially โ€” state management. Because Shadcn components are built on top of Radix UI primitives, they come with their own internal state for things like open/close toggles, selected values, and focus management. However, when you need to coordinate state across multiple components or share data with external systems, you'll need to implement your own state management strategy.

This tutorial explores the various patterns and libraries you can use to manage state effectively in a Shadcn UI-powered application. We'll cover local component state, lifted state, the Context API, and popular state management libraries like Zustand, Jotai, and Redux Toolkit. We'll also look at form state management with React Hook Form and server state with TanStack Query.

Why State Management Matters in Shadcn UI

State management is the backbone of any interactive React application. In a Shadcn UI project, you'll encounter several scenarios where proper state management is essential:

Without a clear state management strategy, your application can quickly become difficult to maintain, with prop drilling, inconsistent state, and hard-to-track bugs.

Understanding Shadcn UI's Component Architecture

Before diving into state management patterns, it's important to understand how Shadcn UI components handle their own state. Most interactive Shadcn components are built on Radix UI primitives, which support both controlled and uncontrolled patterns.

Uncontrolled Components

In uncontrolled mode, the component manages its own state internally. You don't need to pass any state-related props:

import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"

export function UncontrolledDialog() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant="outline">Open Dialog</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Hello World</DialogTitle>
        </DialogHeader>
        <p>This dialog manages its own open state.</p>
      </DialogContent>
    </Dialog>
  )
}

Controlled Components

In controlled mode, you manage the state externally and pass it to the component via props. This is useful when you need to programmatically control the component or synchronize it with other state:

import { useState } from "react"
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"

export function ControlledDialog() {
  const [open, setOpen] = useState(false)

  return (
    <>
      <Button onClick={() => setOpen(true)}>Open Dialog</Button>
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Controlled Dialog</DialogTitle>
          </DialogHeader>
          <p>This dialog's state is controlled externally.</p>
          <Button onClick={() => setOpen(false)}>Close</Button>
        </DialogContent>
      </Dialog>
    </>
  )
}

Understanding this duality is key to choosing the right state management approach for each scenario.

Local State Management with useState

The simplest form of state management in a Shadcn UI application is React's built-in useState hook. This is appropriate for state that is confined to a single component or a small component tree.

Managing a Select Component

import { useState } from "react"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

export function LanguageSelector() {
  const [language, setLanguage] = useState("en")

  return (
    <div className="space-y-2">
      <Select value={language} onValueChange={setLanguage}>
        <SelectTrigger className="w-[180px]">
          <SelectValue placeholder="Select language" />
        </SelectTrigger>
        <SelectContent>
          <SelectItem value="en">English</SelectItem>
          <SelectItem value="es">Spanish</SelectItem>
          <SelectItem value="fr">French</SelectItem>
          <SelectItem value="de">German</SelectItem>
        </SelectContent>
      </Select>
      <p className="text-sm text-muted-foreground">
        Selected: {language}
      </p>
    </div>
  )
}

Managing Tabs

import { useState } from "react"
import {
  Tabs,
  TabsContent,
  TabsList,
  TabsTrigger,
} from "@/components/ui/tabs"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"

export function SettingsTabs() {
  const [activeTab, setActiveTab] = useState("account")

  return (
    <Tabs value={activeTab} onValueChange={setActiveTab} className="w-[400px]">
      <TabsList className="grid w-full grid-cols-3">
        <TabsTrigger value="account">Account</TabsTrigger>
        <TabsTrigger value="password">Password</TabsTrigger>
        <TabsTrigger value="appearance">Appearance</TabsTrigger>
      <TabsList>
      <TabsContent value="account">
        <Card>
          <CardHeader>
            <CardTitle>Account Settings</CardTitle>
          </CardHeader>
          <CardContent>
            <p>Manage your account settings here.</p>
          </CardContent>
        </Card>
      </TabsContent>
      <TabsContent value="password">
        <Card>
          <CardHeader>
            <CardTitle>Password</CardTitle>
          </CardHeader>
          <CardContent>
            <p>Change your password here.</p>
          </CardContent>
        </Card>
      </TabsContent>
      <TabsContent value="appearance">
        <Card>
          <CardHeader>
            <CardTitle>Appearance</CardTitle>
          </CardHeader>
          <CardContent>
            <p>Customize the appearance of the app.</p>
          </CardContent>
        </Card>
      </TabsContent>
    </Tabs>
  )
}

Local state is perfect when the state doesn't need to be shared with other parts of the application. However, as your application grows, you'll need more sophisticated approaches.

Lifted State Pattern

When multiple components need access to the same state, the simplest approach is to lift the state up to their nearest common ancestor. This is a fundamental React pattern that works well with Shadcn UI components.

Example: Coordinated Form and Preview

import { useState } from "react"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"

export function ProfileEditor() {
  const [profile, setProfile] = useState({
    name: "",
    bio: "",
    email: "",
  })

  const handleChange = (field: string, value: string) => {
    setProfile((prev) => ({ ...prev, [field]: value }))
  }

  const handleSave = () => {
    console.log("Saving profile:", profile)
  }

  return (
    <div className="grid grid-cols-2 gap-6">
      <Card>
        <CardHeader>
          <CardTitle>Edit Profile</CardTitle>
        </CardHeader>
        <CardContent className="space-y-4">
          <div className="space-y-2">
            <Label htmlFor="name">Name</Label>
            <Input
              id="name"
              value={profile.name}
              onChange={(e) => handleChange("name", e.target.value)}
            />
          </div>
          <div className="space-y-2">
            <Label htmlFor="email">Email</Label>
            <Input
              id="email"
              type="email"
              value={profile.email}
              onChange={(e) => handleChange("email", e.target.value)}
            />
          </div>
          <div className="space-y-2">
            <Label htmlFor="bio">Bio</Label>
            <Textarea
              id="bio"
              value={profile.bio}
              onChange={(e) => handleChange("bio", e.target.value)}
            />
          </div>
          <Button onClick={handleSave}>Save Changes</Button>
        </CardContent>
      </Card>

      <ProfilePreview profile={profile} />
    </div>
  )
}

function ProfilePreview({ profile }: { profile: { name: string; bio: string; email: string } }) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Live Preview</CardTitle>
      </CardHeader>
      <CardContent>
        <div className="space-y-3">
          <div>
            <h3 className="text-lg font-semibold">
              {profile.name || "Your Name"}
            </h3>
            <p className="text-sm text-muted-foreground">
              {profile.email || "your@email.com"}
            </p>
          </div>
          <p className="text-sm">
            {profile.bio || "Your bio will appear here."}
          </p>
        </div>
      </CardContent>
    </Card>
  )
}

While lifting state works well for small component trees, it can lead to prop drilling when the state needs to be passed through many layers of components.

Using the Context API with Shadcn UI

The React Context API provides a way to share state across components without prop drilling. This is particularly useful for global UI state like themes, sidebar visibility, or command palette state.

Creating a Theme Context

import {
  createContext,
  useContext,
  useEffect,
  useState,
  ReactNode,
} from "react"

type Theme = "light" | "dark" | "system"

type ThemeContextValue = {
  theme: Theme
  setTheme: (theme: Theme) => void
  resolvedTheme: "light" | "dark"
}

const ThemeContext = createContext<ThemeContextValue | undefined>(undefined)

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>(() => {
    if (typeof window !== "undefined") {
      return (localStorage.getItem("theme") as Theme) || "system"
    }
    return "system"
  })

  const [resolvedTheme, setResolvedTheme] = useState<"light" | "dark">("light")

  useEffect(() => {
    const root = window.document.documentElement
    root.classList.remove("light", "dark")

    if (theme === "system") {
      const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
        .matches
        ? "dark"
        : "light"
      root.classList.add(systemTheme)
      setResolvedTheme(systemTheme)
    } else {
      root.classList.add(theme)
      setResolvedTheme(theme)
    }

    localStorage.setItem("theme", theme)
  }, [theme])

  return (
    <ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

export function useTheme() {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error("useTheme must be used within a ThemeProvider")
  }
  return context
}

Using the Theme Context with a Shadcn DropdownMenu

import { Moon, Sun } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { useTheme } from "@/contexts/theme-context"

export function ThemeToggle() {
  const { setTheme } = useTheme()

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="icon">
          <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
          <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
          <span className="sr-only">Toggle theme</span>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onClick={() => setTheme("light")}>
          Light
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("dark")}>
          Dark
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("system")}>
          System
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Creating a Command Palette Context

Command palettes are a common pattern in modern applications. Here's how to manage command palette state using Context:

import {
  createContext,
  useContext,
  useState,
  ReactNode,
} from "react"
import {
  CommandDialog,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command"

type CommandPaletteContextValue = {
  open: boolean
  setOpen: (open: boolean) => void
  toggle: () => void
}

const CommandPaletteContext = createContext<
  CommandPaletteContextValue | undefined
>(undefined)

export function CommandPaletteProvider({ children }: { children: ReactNode }) {
  const [open, setOpen] = useState(false)

  const toggle = () => setOpen((prev) => !prev)

  return (
    <CommandPaletteContext.Provider value={{ open, setOpen, toggle }}>
      {children}
      <CommandDialog open={open} onOpenChange={setOpen}>
        <CommandInput placeholder="Type a command or search..." />
        <CommandList>
          <CommandEmpty>No results found.</CommandEmpty>
          <CommandGroup heading="Suggestions">
            <CommandItem>Calendar</CommandItem>
            <CommandItem>Search Emoji</CommandItem>
            <CommandItem>Calculator</CommandItem>
          </CommandGroup>
        </CommandList>
      </CommandDialog>
    </CommandPaletteContext.Provider>
  )
}

export function useCommandPalette() {
  const context = useContext(CommandPaletteContext)
  if (!context) {
    throw new Error("useCommandPalette must be used within a CommandPaletteProvider")
  }
  return context
}

You can then trigger the command palette from anywhere in your application:

import { useEffect } from "react"
import { useCommandPalette } from "@/contexts/command-palette-context"
import { Button } from "@/components/ui/button"

export function Navbar() {
  const { toggle, setOpen } = useCommandPalette()

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key === "k") {
        e.preventDefault()
        toggle()
      }
    }
    window.addEventListener("keydown", handleKeyDown)
    return () => window.removeEventListener("keydown", handleKeyDown)
  }, [toggle])

  return (
    <nav className="flex items-center justify-between p-4 border-b">
      <h1 className="text-lg font-bold">My App</h1>
      <Button variant="outline" onClick={() => setOpen(true)}>
        Search (โŒ˜K)
      </Button>
    </nav>
  )
}

Zustand Integration with Shadcn UI

Zustand is a lightweight, fast, and scalable state management library that works exceptionally well with Shadcn UI. It provides a simple API with minimal boilerplate and doesn't require providers.

Installing Zustand

npm install zustand

Creating a Cart Store

import { create } from "zustand"
import { persist } from "zustand/middleware"

type CartItem = {
  id: string
  name: string
  price: number
  quantity: number
  image: string
}

type CartStore = {
  items: CartItem[]
  isOpen: boolean
  addItem: (item: Omit<CartItem, "quantity">) => void
  removeItem: (id: string) => void
  updateQuantity: (id: string, quantity: number) => void
  clearCart: () => void
  openCart: () => void
  closeCart: () => void
  toggleCart: () => void
  getTotalItems: () => number
  getTotalPrice: () => number
}

export const useCartStore = create<CartStore>()(
  persist(
    (set, get) => ({
      items: [],
      isOpen: false,

      addItem: (item) =>
        set((state) => {
          const existing = state.items.find((i) => i.id === item.id)
          if (existing) {
            return {
              items: state.items.map((i) =>
                i.id === item.id
                  ? { ...i, quantity: i.quantity + 1 }
                  : i
              ),
            }
          }
          return { items: [...state.items, { ...item, quantity: 1 }] }
        }),

      removeItem: (id) =>
        set((state) => ({
          items: state.items.filter((i) => i.id !== id),
        })),

      updateQuantity: (id, quantity) =>
        set((state) => ({
          items:
            quantity <= 0
              ? state.items.filter((i) => i.id !== id)
              : state.items.map((i) =>
                  i.id === id ? { ...i, quantity } : i
                ),
        })),

      clearCart: () => set({ items: [] }),
      openCart: () => set({ isOpen: true }),
      closeCart: () => set({ isOpen: false }),
      toggleCart: () => set((state) => ({ isOpen: !state.isOpen })),

      getTotalItems: () =>
        get().items.reduce((total, item) => total + item.quantity, 0),

      getTotalPrice: () =>
        get().items.reduce(
          (total, item) => total + item.price * item.quantity,
          0
        ),
    }),
    {
      name: "cart-storage",
      partialize: (state) => ({ items: state.items }),
    }
  )
)

Building a Shopping Cart with Shadcn Sheet

import { Minus, Plus, ShoppingBag, Trash2, X } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
  Sheet,
  SheetContent,
  SheetHeader,
  SheetTitle,
  SheetFooter,
} from "@/components/ui/sheet"
import { Separator } from "@/components/ui/separator"
import { useCartStore } from "@/stores/cart-store"
import { formatCurrency } from "@/lib/utils"

export function CartSheet() {
  const {
    items,
    isOpen,
    closeCart,
    removeItem,
    updateQuantity,
    getTotalPrice,
    clearCart,
  } = useCartStore()

  const total = getTotalPrice()

  return (
    <Sheet open={isOpen} onOpenChange={(open) => !open && closeCart()}>
      <SheetContent className="flex flex-col w-full sm:max-w-lg">
        <SheetHeader>
          <SheetTitle className="flex items-center gap-2">
            <ShoppingBag className="h-5 w-5" />
            Shopping Cart ({items.length})
          </SheetTitle>
        </SheetHeader>

        {items.length === 0 ? (
          <div className="flex-1 flex items-center justify-center">
            <p className="text-muted-foreground">Your cart is empty</p>
          </div>
        ) : (
          <>
            <div className="flex-1 overflow-y-auto space-y-4 py-4">
              {items.map((item) => (
                <div key={item.id} className="flex gap-4">
                  <img
                    src={item.image}
                    alt={item.name}
                    className="h-16 w-16 rounded-md object-cover"
                  />
                  <div className="flex-1 space-y-1">
                    <h4 className="text-sm font-medium">{item.name}</h4>
                    <p className="text-sm text-muted-foreground">
                      {formatCurrency(item.price)}
                    </p>
                    <div className="flex items-center gap-2">
                      <Button
                        size="icon"
                        variant="outline"
                        className="h-7 w-7"
                        onClick={() =>
                          updateQuantity(item.id, item.quantity - 1)
                        }
                      >
                        <Minus className="h-3 w-3" />
                      </Button>
                      <span className="text-sm w-8 text-center">
                        {item.quantity}
                      </span>
                      <Button
                        size="icon"
                        variant="outline"
                        className="h-7 w-7"
                        onClick={() =>
                          updateQuantity(item.id, item.quantity + 1)
                        }
                      >
                        <Plus className="h-3 w-3" />
                      </Button>
                      <Button
                        size="icon"
                        variant="ghost"
                        className="h-7 w-7 ml-auto"
                        onClick={() => removeItem(item.id)}
                      >
                        <Trash2 className="h-3 w-3" />
                      </Button>
                    </div>
                  </div>
                </div>
              ))}
            </div>

            <Separator />

            <SheetFooter>
              <div className="flex justify-between w-full mb-4">
                <span className="font-medium">Total</span>
                <span className="font-bold">{formatCurrency(total)}</span>
              </div>
              <div className="flex gap-2">
                <Button variant="outline" onClick={clearCart} className="flex-1">
                  Clear Cart
                </Button>
                <Button className="flex-1">Checkout</Button>
              </div>
            </SheetFooter>
          </>
        )}
      </SheetContent>
    </Sheet>
  )
}

Adding Items to the Cart

import { ShoppingCart } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { useCartStore } from "@/stores/cart-store"

type Product = {
  id: string
  name: string
  price: number
  image: string
  category: string
}

export function ProductCard({ product }: { product: Product }) {
  const { addItem, openCart } = useCartStore()

  const handleAddToCart = () => {
    addItem(product)
    openCart()
  }

  return (
    <Card className="overflow-hidden">
      <img
        src={product.image}
        alt={product.name}
        className="h-48 w-full object-cover"
      />
      <CardHeader>
        <Badge variant="secondary" className="w-fit">
          {product.category}
        </Badge>
        <CardTitle>{product.name}</CardTitle>
      </CardHeader>
      <CardContent>
        <p className="text-2xl font-bold">
          ${product.price.toFixed(2)}
        </p>
      </CardContent>
      <CardFooter>
        <Button onClick={handleAddToCart} className="w-full">
          <ShoppingCart className="mr-2 h-4 w-4" />
          Add to Cart
        </Button>
      </CardFooter>
    </Card>
  )
}

Jotai Integration with Shadcn UI

Jotai is an atomic state management library that takes a bottom-up approach. Instead of storing state in a single store, you create individual atoms of state that can be composed together. This works particularly well for fine-grained state updates.

Installing Jotai

npm install jotai

Creating Atoms for a Multi-Step Wizard

import { atom, useAtom, useAtomValue } from "jotai"

// Step state
const currentStepAtom = atom(0)

// Form data atoms
const personalInfoAtom = atom({
  firstName: "",
  lastName: "",
  email: "",
})

const addressInfoAtom = atom({
  street: "",
  city: "",
  state: "",
  zipCode: "",
})

const preferencesAtom = atom({
  newsletter: false,
  notifications: true,
  theme: "light" as "light" | "dark",
})

// Derived atom for total steps
const totalStepsAtom = atom(3)

// Derived atom for progress percentage
const progressAtom = atom((get) => {
  const current = get(currentStepAtom)
  const total = get(totalStepsAtom)
  return ((current + 1) / total) * 100
})

// Derived atom for all form data
const allFormDataAtom = atom((get) => ({
  personal: get(personalInfoAtom),
  address: get(addressInfoAtom),
  preferences: get(preferencesAtom),
}))

export {
  currentStepAtom,
  personalInfoAtom,
  addressInfoAtom,
  preferencesAtom,
  totalStepsAtom,
  progressAtom,
  allFormDataAtom,
}

Building the Wizard with Shadcn Components

import { Check } from "lucide-react"
import { useAtom, useAtomValue } from "jotai"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Checkbox } from "@/components/ui/checkbox"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Progress } from "@/components/ui/progress"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import {
  currentStepAtom,
  personalInfoAtom,
  addressInfoAtom,
  preferencesAtom,
  progressAtom,
  allFormDataAtom,
} from "@/atoms/wizard-atoms"

const steps = ["Personal", "Address", "Preferences"]

export function MultiStepWizard() {
  const [currentStep, setCurrentStep] = useAtom(currentStepAtom)
  const progress = useAtomValue(progressAtom)

  const handleNext = () => {
    if (currentStep < steps.length - 1) {
      setCurrentStep(currentStep + 1)
    }
  }

  const handleBack = () => {
    if (currentStep > 0) {
      setCurrentStep(currentStep - 1)
    }
  }

  const handleSubmit = () => {
    const data = useAtomValue(allFormDataAtom)
    console.log("Form submitted:", data)
  }

  return (
    <div className="max-w-2xl mx-auto space-y-6">
      <div className="space-y-2">
        <div className="flex justify-between">
          {steps.map((step, index) => (
            <div key={step} className="flex items-center gap-2">
              <div
                className={`flex h-8 w-8 items-center justify-center rounded-full border-2 ${
                  index <= currentStep
                    ? "border-primary bg-primary text-primary-foreground"
                    : "border-muted-foreground/30 text-muted-foreground"
                }`}
              >
                {index < currentStep ? (
                  <Check className="h-4 w-4" />
                ) : (
                  index + 1
                )}
              </div>
              <span className="text-sm font-medium hidden sm:block">
                {step}
              </span>
            </div>
          ))}
        </div>
        <Progress value={progress} />
      </div>

      <Card>
        <CardHeader>
          <CardTitle>{steps[currentStep]} Information</CardTitle>
        </CardHeader>
        <CardContent>
          {currentStep === 0 && <PersonalInfoStep />}
          {currentStep === 1 && <AddressInfoStep />}
          {currentStep === 2 && <PreferencesStep />}
        </CardContent>
      </Card>

      <div className="flex justify-between">
        <Button
          variant="outline"
          onClick={handleBack}
          disabled={currentStep === 0}
        >
          Back
        </Button>
        {currentStep < steps.length - 1 ? (
          <Button onClick={handleNext}>Next</Button>
        ) : (
          <Button onClick={handleSubmit}>Submit</Button>
        )}
      </div>
    </div>
  )
}

function PersonalInfoStep() {
  const [info, setInfo] = useAtom(personalInfoAtom)

  return (
    <div className="space-y-4">
      <div className="grid grid-cols-2 gap-4">
        <div className="space-y-2">
          <Label htmlFor="firstName">First Name</Label>
          <Input
            id="firstName"
            value={info.firstName}
            onChange={(e) =>
              setInfo({ ...info, firstName: e.target.value })
            }
          />
        </div>
        <div className="space-y-2">
          <Label htmlFor="lastName">Last Name</Label>
          <Input
            id="lastName"
            value={info.lastName}
            onChange={(e) =>
              setInfo({ ...info, lastName: e.target.value })
            }
          />
        </div>
      </div>
      <div className="space-y-2">
        <Label htmlFor="email">Email</Label>
        <Input
          id="email"
          type="email"
          value={info.email}
          onChange

๐Ÿ›  Tools from DevBytes

Inventory Tracker Pro โ€” Excel inventory system, low-stock alerts ยท $19
AI Dev Kit for Mac โ€” local AI dev environment templates ยท $9.99
KeyMapper for Mac โ€” custom keyboard shortcut toolkit ยท $7.99

โ† Back to all articles