Help us improve
Share bugs, ideas, or general feedback.
From fullstack-iac
Provides React 18+ patterns with Vite bundler, TypeScript, hooks, component design, and state management including Zustand and React Query for frontend development.
npx claudepluginhub markus41/claude --plugin fullstack-iacHow this skill is triggered — by the user, by Claude, or both
Slash command
/fullstack-iac:react-viteThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Provides comprehensive React 18+ development with Vite bundler, TypeScript integration, and modern frontend patterns.
React 19 + Vite SPA guidelines covering composition patterns, performance optimization, performance optimization, and web interface best practices. Triggers on React hooks, lazy loading, Vite config, and component architecture discussions.
Provides React code patterns for functional components, custom hooks, server/client components, form handling, and context. Useful when editing .tsx/.jsx files, components, hooks, or React upgrades.
Builds React 19 components and Next.js 15 apps with responsive layouts, client-side state management using Zustand, and server components. Optimizes performance, accessibility, and data fetching.
Share bugs, ideas, or general feedback.
Provides comprehensive React 18+ development with Vite bundler, TypeScript integration, and modern frontend patterns.
Activate this skill when working with:
# Create new Vite + React + TypeScript project
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
# Install common dependencies
npm install @tanstack/react-query zustand react-hook-form zod
npm install -D @types/node
# Development
npm run dev
# Build
npm run build
npm run preview
# Lint
npm run lint
src/
├── main.tsx # Application entry point
├── App.tsx # Root component
├── vite-env.d.ts # Vite TypeScript definitions
├── components/ # Reusable components
│ ├── ui/ # Base UI components
│ │ ├── Button.tsx
│ │ ├── Card.tsx
│ │ └── Input.tsx
│ └── features/ # Feature-specific components
│ ├── UserList.tsx
│ └── Dashboard.tsx
├── hooks/ # Custom hooks
│ ├── useAuth.ts
│ ├── useApi.ts
│ └── useLocalStorage.ts
├── stores/ # State management
│ ├── authStore.ts
│ └── userStore.ts
├── services/ # API and external services
│ ├── api.ts
│ └── auth.ts
├── types/ # TypeScript types
│ ├── index.ts
│ └── api.ts
├── utils/ # Utility functions
│ └── helpers.ts
└── styles/ # Global styles
└── index.css
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
// Path aliases
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@hooks': path.resolve(__dirname, './src/hooks'),
'@stores': path.resolve(__dirname, './src/stores'),
'@types': path.resolve(__dirname, './src/types'),
'@utils': path.resolve(__dirname, './src/utils'),
},
},
// Server configuration
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
},
},
},
// Build optimization
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['@tanstack/react-query', 'zustand'],
},
},
},
sourcemap: true,
minify: 'terser',
},
})
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@hooks/*": ["./src/hooks/*"],
"@stores/*": ["./src/stores/*"],
"@types/*": ["./src/types/*"],
"@utils/*": ["./src/utils/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
import { FC, ReactNode } from 'react'
interface ButtonProps {
children: ReactNode
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
onClick?: () => void
}
export const Button: FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'md',
disabled = false,
onClick,
}) => {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
)
}
interface ListProps<T> {
items: T[]
renderItem: (item: T) => ReactNode
keyExtractor: (item: T) => string | number
}
export function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
)
}
// Usage
<List
items={users}
renderItem={(user) => <UserCard user={user} />}
keyExtractor={(user) => user.id}
/>
import { useState, useEffect } from 'react'
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.error(error)
return initialValue
}
})
const setValue = (value: T) => {
try {
setStoredValue(value)
window.localStorage.setItem(key, JSON.stringify(value))
} catch (error) {
console.error(error)
}
}
return [storedValue, setValue]
}
import { useState, useEffect } from 'react'
export function useDebounce<T>(value: T, delay: number = 500): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler)
}
}, [value, delay])
return debouncedValue
}
import { useState, useEffect, useCallback } from 'react'
interface AsyncState<T> {
data: T | null
loading: boolean
error: Error | null
}
export function useAsync<T>(
asyncFunction: () => Promise<T>,
immediate = true
) {
const [state, setState] = useState<AsyncState<T>>({
data: null,
loading: immediate,
error: null,
})
const execute = useCallback(async () => {
setState({ data: null, loading: true, error: null })
try {
const response = await asyncFunction()
setState({ data: response, loading: false, error: null })
} catch (error) {
setState({ data: null, loading: false, error: error as Error })
}
}, [asyncFunction])
useEffect(() => {
if (immediate) {
execute()
}
}, [execute, immediate])
return { ...state, execute }
}
// stores/authStore.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface User {
id: string
email: string
name: string
}
interface AuthState {
user: User | null
token: string | null
isAuthenticated: boolean
login: (user: User, token: string) => void
logout: () => void
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
user: null,
token: null,
isAuthenticated: false,
login: (user, token) =>
set({ user, token, isAuthenticated: true }),
logout: () =>
set({ user: null, token: null, isAuthenticated: false }),
}),
{
name: 'auth-storage',
}
)
)
// Usage in component
function Profile() {
const { user, logout } = useAuthStore()
return (
<div>
<h1>{user?.name}</h1>
<button onClick={logout}>Logout</button>
</div>
)
}
// services/api.ts
import axios from 'axios'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || '/api',
})
api.interceptors.request.use((config) => {
const token = useAuthStore.getState().token
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
export interface User {
id: number
name: string
email: string
}
export const userApi = {
getAll: () => api.get<User[]>('/users').then((res) => res.data),
getById: (id: number) => api.get<User>(`/users/${id}`).then((res) => res.data),
create: (data: Omit<User, 'id'>) => api.post<User>('/users', data).then((res) => res.data),
update: (id: number, data: Partial<User>) => api.patch<User>(`/users/${id}`, data).then((res) => res.data),
delete: (id: number) => api.delete(`/users/${id}`),
}
// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { userApi, User } from '@/services/api'
export function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: userApi.getAll,
staleTime: 5 * 60 * 1000, // 5 minutes
})
}
export function useUser(id: number) {
return useQuery({
queryKey: ['users', id],
queryFn: () => userApi.getById(id),
enabled: !!id,
})
}
export function useCreateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: userApi.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
},
})
}
export function useUpdateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ id, data }: { id: number; data: Partial<User> }) =>
userApi.update(id, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['users'] })
queryClient.invalidateQueries({ queryKey: ['users', variables.id] })
},
})
}
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const userSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email address'),
age: z.number().min(18, 'Must be at least 18'),
})
type UserFormData = z.infer<typeof userSchema>
export function UserForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset,
} = useForm<UserFormData>({
resolver: zodResolver(userSchema),
defaultValues: {
name: '',
email: '',
age: 18,
},
})
const { mutate: createUser } = useCreateUser()
const onSubmit = (data: UserFormData) => {
createUser(data, {
onSuccess: () => {
reset()
},
})
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="name">Name</label>
<input {...register('name')} id="name" />
{errors.name && <span>{errors.name.message}</span>}
</div>
<div>
<label htmlFor="email">Email</label>
<input {...register('email')} type="email" id="email" />
{errors.email && <span>{errors.email.message}</span>}
</div>
<div>
<label htmlFor="age">Age</label>
<input {...register('age', { valueAsNumber: true })} type="number" id="age" />
{errors.age && <span>{errors.age.message}</span>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Creating...' : 'Create User'}
</button>
</form>
)
}
import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Users = lazy(() => import('./pages/Users'))
const Settings = lazy(() => import('./pages/Settings'))
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/users" element={<Users />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
)
}
import { memo, useMemo, useCallback } from 'react'
interface UserListProps {
users: User[]
onUserClick: (id: number) => void
}
export const UserList = memo<UserListProps>(({ users, onUserClick }) => {
const sortedUsers = useMemo(
() => [...users].sort((a, b) => a.name.localeCompare(b.name)),
[users]
)
const handleClick = useCallback(
(id: number) => {
console.log('Clicked user:', id)
onUserClick(id)
},
[onUserClick]
)
return (
<ul>
{sortedUsers.map((user) => (
<li key={user.id} onClick={() => handleClick(user.id)}>
{user.name}
</li>
))}
</ul>
)
})
# .env
VITE_API_URL=http://localhost:8000/api
VITE_APP_NAME=Zenith
VITE_ENABLE_ANALYTICS=true
// Access in code
const apiUrl = import.meta.env.VITE_API_URL
const appName = import.meta.env.VITE_APP_NAME
const isDev = import.meta.env.DEV
const isProd = import.meta.env.PROD
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
// src/test/setup.ts
import { expect, afterEach } from 'vitest'
import { cleanup } from '@testing-library/react'
import * as matchers from '@testing-library/jest-dom/matchers'
expect.extend(matchers)
afterEach(() => {
cleanup()
})
any typesmemo, useMemo, useCallback judiciously# Build for production
npm run build
# Preview production build
npm run preview
# Analyze bundle size
npm run build -- --mode analyze
# Type checking
npx tsc --noEmit
# Common Vite plugins
npm install -D vite-plugin-pwa
npm install -D vite-plugin-compression
npm install -D @vitejs/plugin-react-swc # Faster than default