Skip to content

React ​

Learn how to use Galaxy UI with React 18+.

Requirements ​

  • React: 18.0.0 or higher
  • TypeScript: 5.0.0 or higher (recommended)
  • Tailwind CSS: 3.4.0 or higher
  • Vite: 5.0.0 or higher (recommended) or Create React App

Installation ​

Initialize Galaxy UI in your React project:

bash
npx @galaxy-stack/nebula-cli@latest init
bash
pnpm dlx @galaxy-stack/nebula-cli@latest init
bash
yarn dlx @galaxy-stack/nebula-cli@latest init
bash
bunx @galaxy-stack/nebula-cli@latest init

The CLI will:

  1. Detect that you're using React
  2. Install required dependencies
  3. Create components.json
  4. Setup Tailwind CSS configuration
  5. Create utility functions

Project Setup ​

Create a new project with Vite:

bash
# Create project
npm create vite@latest my-app -- --template react-ts

# Navigate to project
cd my-app

# Install dependencies
npm install

# Initialize Galaxy UI
npx @galaxy-stack/nebula-cli@latest init

Vite Configuration ​

Ensure your vite.config.ts has path aliases:

ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})

TypeScript Configuration ​

Update tsconfig.json:

json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    },

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"]
}

Adding Components ​

Add components to your project:

bash
npx @galaxy-stack/nebula-cli@latest add button

This will:

  1. Create src/components/ui/button/ directory
  2. Copy component files (Button.tsx, variants.ts, index.ts)
  3. Install dependencies (@radix-ui/react-slot, class-variance-authority)
  4. Export Button component with props: variant (6 options), size (4 options), asChild, className, type, disabled, onClick, children

Usage ​

Basic Example ​

tsx
import { Button } from '@/components/ui/button'

function App() {
  return (
    <div className="p-4">
      <Button>Click me</Button>
    </div>
  )
}

export default App

With Variants ​

tsx
import { Button } from '@/components/ui/button'

function App() {
  return (
    <div className="flex gap-2">
      <Button variant="default">Default</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="destructive">Destructive</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="link">Link</Button>
    </div>
  )
}

With Sizes ​

tsx
import { Button } from '@/components/ui/button'

function App() {
  return (
    <div className="flex items-center gap-2">
      <Button size="sm">Small</Button>
      <Button size="default">Default</Button>
      <Button size="lg">Large</Button>
    </div>
  )
}

Form Example ​

tsx
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'

function LoginForm() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    console.log({ email, password })
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4 max-w-md">
      <div className="space-y-2">
        <Label htmlFor="email">Email</Label>
        <Input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="Enter your email"
        />
      </div>

      <div className="space-y-2">
        <Label htmlFor="password">Password</Label>
        <Input
          id="password"
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="Enter your password"
        />
      </div>

      <Button type="submit" className="w-full">
        Sign In
      </Button>
    </form>
  )
}

Dialog Example ​

tsx
import { useState } from 'react'
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogFooter,
  DialogTitle,
  DialogDescription,
  DialogTrigger,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'

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

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>
        <Button>Open Dialog</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Are you sure?</DialogTitle>
          <DialogDescription>
            This action cannot be undone. This will permanently delete your account.
          </DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <Button variant="outline" onClick={() => setOpen(false)}>
            Cancel
          </Button>
          <Button variant="destructive" onClick={() => setOpen(false)}>
            Delete
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

React Hooks ​

useToast (Coming Soon) ​

For notifications and alerts:

tsx
import { useToast } from '@/hooks/use-toast'

function MyComponent() {
  const { toast } = useToast()

  return (
    <Button
      onClick={() => {
        toast({
          title: "Success",
          description: "Your changes have been saved.",
        })
      }}
    >
      Save Changes
    </Button>
  )
}

React Router Integration ​

With React Router v6 ​

tsx
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'
import { Button } from '@/components/ui/button'

function App() {
  return (
    <BrowserRouter>
      <nav className="flex gap-2 p-4">
        <Button asChild variant="ghost">
          <Link to="/">Home</Link>
        </Button>
        <Button asChild variant="ghost">
          <Link to="/about">About</Link>
        </Button>
      </nav>

      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/about" element={<AboutPage />} />
      </Routes>
    </BrowserRouter>
  )
}

Form Libraries ​

With React Hook Form ​

tsx
import { useForm } from 'react-hook-form'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'

type FormData = {
  email: string
  password: string
}

function LoginForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<FormData>()

  const onSubmit = (data: FormData) => {
    console.log(data)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <div className="space-y-2">
        <Label htmlFor="email">Email</Label>
        <Input
          id="email"
          {...register('email', { required: true })}
          placeholder="Enter your email"
        />
        {errors.email && <span className="text-sm text-destructive">Email is required</span>}
      </div>

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

State Management ​

With Zustand ​

tsx
import { create } from 'zustand'
import { Button } from '@/components/ui/button'

type Store = {
  count: number
  increment: () => void
  decrement: () => void
}

const useStore = create<Store>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}))

function Counter() {
  const { count, increment, decrement } = useStore()

  return (
    <div className="flex items-center gap-4">
      <Button onClick={decrement}>-</Button>
      <span className="text-2xl font-bold">{count}</span>
      <Button onClick={increment}>+</Button>
    </div>
  )
}

TypeScript Support ​

All components are fully typed with TypeScript:

tsx
import { Button, type ButtonProps } from '@/components/ui/button'
import { ComponentPropsWithoutRef } from 'react'

// Extend component props
type CustomButtonProps = ButtonProps & {
  loading?: boolean
}

function CustomButton({ loading, children, ...props }: CustomButtonProps) {
  return (
    <Button disabled={loading} {...props}>
      {loading ? 'Loading...' : children}
    </Button>
  )
}

Best Practices ​

Component Composition ​

tsx
// ✅ Good: Compose components
import { Button } from '@/components/ui/button'
import { Spinner } from '@/components/ui/spinner'

function LoadingButton({ loading, children, ...props }) {
  return (
    <Button disabled={loading} {...props}>
      {loading && <Spinner size="sm" className="mr-2" />}
      {children}
    </Button>
  )
}

Using asChild ​

tsx
// ✅ Good: Use asChild for custom elements
<Button asChild>
  <a href="/link">Link Button</a>
</Button>

// ❌ Bad: Nesting buttons
<Button>
  <a href="/link">Link Button</a>
</Button>

Handling Events ​

tsx
// ✅ Good: Type-safe event handlers
<Button onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
  e.preventDefault()
  // Your logic
}}>
  Click me
</Button>

Troubleshooting ​

Module not found errors ​

bash
# Clear cache and reinstall
rm -rf node_modules .vite
npm install

TypeScript errors ​

  1. Check tsconfig.json has correct paths configuration
  2. Restart TypeScript server in VS Code: Cmd+Shift+P → "TypeScript: Restart TS Server"

Styles not applying ​

  1. Verify Tailwind CSS is configured correctly
  2. Check tailwind.config.js content paths include components
  3. Restart dev server

Next Steps ​

Released under the MIT License.