
React Skills
- 58 installs
- 835 repo stars
- Updated June 10, 2026
- llama-farm/llamafarm
Helps with frontend development tasks.
About
react-skills is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted development.
- react-skills
- Frontend Development
- AI-coding skill
React Skills by the numbers
- 58 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #1,228 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/llama-farm/llamafarm --skill react-skillsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 58 |
|---|---|
| repo stars | ★ 835 |
| Last updated | June 10, 2026 |
| Repository | llama-farm/llamafarm ↗ |
What it does
Helps with frontend development tasks.
Files
React Skills for LlamaFarm Designer
Best practices and patterns for React 18 development in the Designer subsystem.
Tech Stack
- React 18.2 with StrictMode
- TypeScript 5.2+ for type safety
- TanStack Query v5 for server state management
- React Router v7 for client-side routing
- TailwindCSS with
tailwind-mergeandclsxfor styling - Radix UI primitives for accessible components
- Vite for build tooling
- Vitest + React Testing Library for testing
Directory Structure
designer/src/
api/ # API service functions
components/ # React components (feature-organized)
contexts/ # React context providers
hooks/ # Custom hooks
lib/ # Utility functions (cn, etc.)
types/ # TypeScript type definitions
utils/ # Helper functions
test/ # Test utilities and mocksCore Patterns
Component Composition
- Use composition over inheritance
- Prefer small, focused components
- Use
forwardReffor components that wrap DOM elements - Apply
displayNameto forwardRef components for DevTools
State Management
- Local UI state:
useState,useReducer - Server state: TanStack Query (
useQuery,useMutation) - Shared UI state: React Context with custom hooks
- Form state: Controlled components with validation
Hooks
- Follow Rules of Hooks (top-level, consistent order)
- Create custom hooks for reusable logic
- Use query key factories for TanStack Query
- Memoize expensive computations with
useMemo - Stabilize callbacks with
useCallback
Styling
- Use
cn()fromlib/utilsto merge Tailwind classes - Use
cva(class-variance-authority) for component variants - Follow dark mode conventions with
dark:prefix
Related Guides
- components.md - Component patterns
- hooks.md - Hook patterns and rules
- state.md - State management patterns
- performance.md - Performance optimization
- security.md - Security best practices
Quick Reference
// Utility for merging Tailwind classes
import { cn } from '@/lib/utils'
cn('base-class', condition && 'conditional-class', className)
// Query key factory pattern
export const projectKeys = {
all: ['projects'] as const,
lists: () => [...projectKeys.all, 'list'] as const,
list: (ns: string) => [...projectKeys.lists(), ns] as const,
}
// Context with validation hook
const MyContext = createContext<MyContextType | undefined>(undefined)
export function useMyContext() {
const ctx = useContext(MyContext)
if (!ctx) throw new Error('useMyContext must be within MyProvider')
return ctx
}Testing
import { renderWithProviders } from '@/test/utils'
import { screen } from '@testing-library/react'
test('renders component', () => {
renderWithProviders(<MyComponent />)
expect(screen.getByText('Hello')).toBeInTheDocument()
})React Component Patterns
Guidelines for building maintainable, accessible React components.
Component Structure
Function Components
Always use function components with TypeScript interfaces:
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost'
size?: 'sm' | 'md' | 'lg'
}
export function Button({ variant = 'primary', size = 'md', className, ...props }: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
)
}forwardRef Components
Use forwardRef when wrapping DOM elements or Radix primitives:
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'Checklist Items
1. Components Must Use TypeScript Interfaces
| Attribute | Value |
|---|---|
| Description | All components should have explicit TypeScript prop interfaces |
| Search Pattern | grep -E "function\s+[A-Z]\w+\s*\(" --include="*.tsx" |
| Pass Criteria | Props are typed via interface or type alias, not inline |
| Severity | High |
| Fix | Extract inline types to named interfaces above component |
2. forwardRef Components Need displayName
| Attribute | Value |
|---|---|
| Description | forwardRef components must set displayName for DevTools |
| Search Pattern | grep -B5 -A1 "forwardRef" --include="*.tsx" |
| Pass Criteria | Every forwardRef has corresponding .displayName = 'ComponentName' |
| Severity | Medium |
| Fix | Add ComponentName.displayName = 'ComponentName' after definition |
3. Spread Props Last
| Attribute | Value |
|---|---|
| Description | Spread ...props should come last to allow overrides |
| Search Pattern | grep -E "\{\.\.\.props.*," --include="*.tsx" |
| Pass Criteria | No destructured props after ...props spread |
| Severity | Medium |
| Fix | Reorder to put {...props} at the end of JSX attributes |
4. Use Composition Over Conditionals
| Attribute | Value |
|---|---|
| Description | Prefer compound components over complex conditional rendering |
| Search Pattern | grep -E "^\s*\{.*\?.*:.*\?.*:.*\}" --include="*.tsx" |
| Pass Criteria | No deeply nested ternaries (max 1 level) |
| Severity | Medium |
| Fix | Extract conditions into separate components or use early returns |
5. Avoid Inline Function Definitions in JSX
| Attribute | Value |
|---|---|
| Description | Event handlers should be defined outside JSX for readability |
| Search Pattern | grep -E "on[A-Z]\w+=\{.*=>" --include="*.tsx" |
| Pass Criteria | Complex handlers (>1 expression) are extracted to named functions |
| Severity | Low |
| Fix | Extract to const handleClick = useCallback(() => {...}, [deps]) |
6. Use cn() for Class Merging
| Attribute | Value |
|---|---|
| Description | Use cn() utility for Tailwind class merging, not template literals |
| Search Pattern | grep -E "className=\{\" --include="*.tsx"` |
| Pass Criteria | Template literals only for simple string interpolation, not conditionals |
| Severity | Low |
| Fix | Replace with cn('base', condition && 'conditional') |
7. Accessible Interactive Elements
| Attribute | Value |
|---|---|
| Description | Buttons and interactive elements need accessible labels |
| Search Pattern | grep -E "<button[^>]*>" --include="*.tsx" |
| Pass Criteria | Buttons have visible text, aria-label, or aria-labelledby |
| Severity | High |
| Fix | Add aria-label for icon-only buttons |
8. Key Props on List Items
| Attribute | Value |
|---|---|
| Description | Mapped elements must have stable, unique key props |
| Search Pattern | grep -E "\.map\(" --include="*.tsx" |
| Pass Criteria | All .map() callbacks return elements with key prop using stable IDs |
| Severity | Critical |
| Fix | Use unique identifier (id, not index) as key |
Component Variants with CVA
Use class-variance-authority for component variants:
import { cva, type VariantProps } from 'class-variance-authority'
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
ghost: 'hover:bg-accent hover:text-accent-foreground',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-8 px-3 text-xs',
lg: 'h-11 px-8',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}Compound Components
For complex components, use the compound component pattern:
const Tabs = ({ children }: { children: React.ReactNode }) => (
<div className="tabs">{children}</div>
)
const TabList = ({ children }: { children: React.ReactNode }) => (
<div role="tablist">{children}</div>
)
const TabPanel = ({ children }: { children: React.ReactNode }) => (
<div role="tabpanel">{children}</div>
)
Tabs.List = TabList
Tabs.Panel = TabPanel
// Usage
<Tabs>
<Tabs.List>...</Tabs.List>
<Tabs.Panel>...</Tabs.Panel>
</Tabs>Provider Composition
Compose providers from the outside in (outermost wraps everything):
// main.tsx - correct order
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</ThemeProvider>
</QueryClientProvider>
</React.StrictMode>React Hooks Patterns
Guidelines for writing and using React hooks effectively.
Rules of Hooks
1. Only call hooks at the top level - Never inside loops, conditions, or nested functions 2. Only call hooks from React functions - Components or custom hooks only 3. Use the `use` prefix - Custom hooks must start with use
Custom Hook Patterns
Basic Custom Hook
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false)
useEffect(() => {
const mql = window.matchMedia(query)
const onChange = (e: MediaQueryListEvent) => setMatches(e.matches)
setMatches(mql.matches)
mql.addEventListener('change', onChange)
return () => mql.removeEventListener('change', onChange)
}, [query])
return matches
}Composed Hooks
export function useIsMobile(): boolean {
return useMediaQuery('(max-width: 767px)')
}Checklist Items
1. Hooks Called at Top Level
| Attribute | Value |
|---|---|
| Description | Hooks must not be called conditionally or in loops |
| Search Pattern | grep -E "if\s*\(.*\)\s*\{[^}]*use[A-Z]" --include="*.ts" --include="*.tsx" |
| Pass Criteria | No hooks inside if/for/while blocks |
| Severity | Critical |
| Fix | Move hook call to top level, conditionally use its return value instead |
2. Custom Hooks Use use Prefix
| Attribute | Value |
|---|---|
| Description | Custom hooks must be named with use prefix |
| Search Pattern | grep -l "export.*function.*use" hooks/*.ts |
| Pass Criteria | All exported hook functions start with use |
| Severity | High |
| Fix | Rename function to start with use |
3. useEffect Has Cleanup
| Attribute | Value |
|---|---|
| Description | Effects with subscriptions/timers must return cleanup function |
| Search Pattern | grep -B2 -A10 "useEffect\(" --include="*.ts" --include="*.tsx" |
| Pass Criteria | addEventListener, setInterval, setTimeout have corresponding cleanup |
| Severity | High |
| Fix | Return cleanup function: return () => { removeEventListener(...) } |
4. useEffect Dependencies Are Complete
| Attribute | Value |
|---|---|
| Description | All variables used in useEffect should be in dependency array |
| Search Pattern | grep -A15 "useEffect\(" --include="*.ts" --include="*.tsx" |
| Pass Criteria | ESLint exhaustive-deps rule passes |
| Severity | High |
| Fix | Add missing deps or wrap with useCallback/useMemo if intentional |
5. useCallback for Handler Props
| Attribute | Value |
|---|---|
| Description | Callbacks passed to children should be memoized |
| Search Pattern | grep -E "on[A-Z]\w+=\{[a-z]\w+\}" --include="*.tsx" |
| Pass Criteria | Handler functions passed to child components use useCallback |
| Severity | Medium |
| Fix | Wrap handler with useCallback(handler, [deps]) |
6. useMemo for Expensive Computations
| Attribute | Value |
|---|---|
| Description | Expensive computations should be memoized |
| Search Pattern | `grep -E "\.(filter |
| Pass Criteria | Large data transformations wrapped in useMemo |
| Severity | Medium |
| Fix | Wrap with useMemo(() => computation, [deps]) |
7. useState Initializer for Expensive Initial Values
| Attribute | Value |
|---|---|
| Description | Expensive initial state should use lazy initializer |
| Search Pattern | grep -E "useState\([^)]+\(" --include="*.ts" --include="*.tsx" |
| Pass Criteria | Function calls in useState use lazy form: useState(() => fn()) |
| Severity | Medium |
| Fix | Change useState(expensiveFn()) to useState(() => expensiveFn()) |
8. useRef for Mutable Values That Don't Trigger Rerenders
| Attribute | Value |
|---|---|
| Description | Values that change but shouldn't trigger rerenders use useRef |
| Search Pattern | grep -E "useRef\(" --include="*.ts" --include="*.tsx" |
| Pass Criteria | Refs used for DOM elements, timers, previous values, mutable flags |
| Severity | Low |
| Fix | Use useRef for values that change without needing rerender |
TanStack Query Hook Patterns
Query Key Factories
export const projectKeys = {
all: ['projects'] as const,
lists: () => [...projectKeys.all, 'list'] as const,
list: (namespace: string) => [...projectKeys.lists(), namespace] as const,
details: () => [...projectKeys.all, 'detail'] as const,
detail: (namespace: string, id: string) =>
[...projectKeys.details(), namespace, id] as const,
}Query Hook
export function useProjects(namespace: string) {
return useQuery({
queryKey: projectKeys.list(namespace),
queryFn: () => projectService.listProjects(namespace),
enabled: !!namespace,
staleTime: 5 * 60 * 1000,
retry: 1,
})
}Mutation Hook
export function useCreateProject() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ namespace, request }) =>
projectService.createProject(namespace, request),
onSuccess: (data, variables) => {
queryClient.invalidateQueries({
queryKey: projectKeys.list(variables.namespace)
})
queryClient.setQueryData(
projectKeys.detail(variables.namespace, data.project.name),
{ project: data.project }
)
},
onError: (error) => {
console.error('Failed to create project:', error)
},
})
}Context Hook Pattern
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
export function useTheme() {
const context = useContext(ThemeContext)
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider')
}
return context
}Cleanup Patterns
Timer Cleanup
useEffect(() => {
const timer = setTimeout(() => {
setExpired(true)
}, 5000)
return () => clearTimeout(timer)
}, [])Event Listener Cleanup
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth)
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])AbortController for Fetch
useEffect(() => {
const controller = new AbortController()
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') throw err
})
return () => controller.abort()
}, [url])RAF Cleanup
useEffect(() => {
const rafRef = { current: null as number | null }
const animate = () => {
// animation logic
rafRef.current = requestAnimationFrame(animate)
}
rafRef.current = requestAnimationFrame(animate)
return () => {
if (rafRef.current) cancelAnimationFrame(rafRef.current)
}
}, [])React Performance Optimization
Guidelines for optimizing React application performance.
Core Principles
1. Measure first - Use React DevTools Profiler before optimizing 2. Avoid premature optimization - Only optimize when there's a measurable problem 3. Minimize rerenders - Prevent unnecessary component updates 4. Code split - Load code only when needed
Memoization Patterns
useMemo for Expensive Computations
const { thinking, contentWithoutThinking } = useMemo(
() => parseMessageContentMemo(content, type, message.id),
[content, type, message.id]
)useCallback for Stable References
const handleSendClick = useCallback(async () => {
const messageContent = inputValue.trim()
if (!canSend || !messageContent) return
setWantsAutoScroll(true)
const success = await sendMessage(messageContent)
if (success) updateInput('')
}, [inputValue, canSend, sendMessage, updateInput])React.memo for Pure Components
const Message = React.memo(function Message({ message }: MessageProps) {
// Component only rerenders if message prop changes
return <div>{message.content}</div>
})Checklist Items
1. Expensive Computations Are Memoized
| Attribute | Value |
|---|---|
| Description | Array transformations and parsing should use useMemo |
| Search Pattern | `grep -E "\.(filter |
| Pass Criteria | Expensive operations inside render wrapped with useMemo |
| Severity | Medium |
| Fix | Wrap with useMemo(() => computation, [deps]) |
2. Callbacks Passed to Children Are Stable
| Attribute | Value |
|---|---|
| Description | Functions passed as props should use useCallback |
| Search Pattern | grep -E "on[A-Z]\w+=\{" --include="*.tsx" |
| Pass Criteria | Callback props use useCallback or are defined outside component |
| Severity | Medium |
| Fix | Wrap with useCallback(fn, [deps]) |
3. Context Values Are Memoized
| Attribute | Value |
|---|---|
| Description | Context provider values should be memoized |
| Search Pattern | grep -B5 "Provider value=" --include="*.tsx" |
| Pass Criteria | Value prop uses useMemo or is a stable reference |
| Severity | High |
| Fix | Wrap value with useMemo(() => ({ ... }), [deps]) |
4. Large Lists Use Virtualization
| Attribute | Value |
|---|---|
| Description | Lists with 100+ items should use virtualization |
| Search Pattern | grep -E "\.map\(" --include="*.tsx" |
| Pass Criteria | Large lists use react-window or similar |
| Severity | Medium |
| Fix | Implement virtualization with react-window |
5. Images Are Lazy Loaded
| Attribute | Value |
|---|---|
| Description | Off-screen images should use lazy loading |
| Search Pattern | grep -E "<img" --include="*.tsx" |
| Pass Criteria | Images have loading="lazy" or use Intersection Observer |
| Severity | Low |
| Fix | Add loading="lazy" attribute |
6. Code Splitting at Route Level
| Attribute | Value |
|---|---|
| Description | Routes should use React.lazy for code splitting |
| Search Pattern | grep -E "import.*from.*components" App.tsx |
| Pass Criteria | Large route components use React.lazy |
| Severity | Medium |
| Fix | Use const Component = React.lazy(() => import('./Component')) |
Current State: The Designer currently uses static imports for all routes. This is a future optimization opportunity for large bundles.
7. Avoid Inline Object/Array Literals in JSX
| Attribute | Value |
|---|---|
| Description | Inline objects create new references on every render |
| Search Pattern | grep -E "style=\{\{" --include="*.tsx" |
| Pass Criteria | Static styles extracted to constants or CSS |
| Severity | Low |
| Fix | Extract to const outside component or use Tailwind |
8. useTransition for Non-Urgent Updates
| Attribute | Value |
|---|---|
| Description | Large state updates should use useTransition |
| Search Pattern | grep -E "useState.*\[\]" --include="*.tsx" |
| Pass Criteria | Bulk updates that affect many components use startTransition |
| Severity | Low |
| Fix | Wrap update with startTransition(() => setState(...)) |
Lazy Loading Patterns
React.lazy with Suspense
const Dashboard = React.lazy(() => import('./components/Dashboard'))
const Models = React.lazy(() => import('./components/Models'))
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/models" element={<Models />} />
</Routes>
</Suspense>
)
}Named Exports with Lazy
const Dashboard = React.lazy(() =>
import('./components/Dashboard').then(module => ({
default: module.Dashboard
}))
)Scroll Performance
RAF Debouncing
const rafRef = useRef<number | null>(null)
const handleScroll = useCallback(() => {
if (rafRef.current) {
cancelAnimationFrame(rafRef.current)
}
rafRef.current = requestAnimationFrame(() => {
const atBottom = checkIfAtBottom()
setIsUserAtBottom(atBottom)
})
}, [checkIfAtBottom])
useEffect(() => {
return () => {
if (rafRef.current) {
cancelAnimationFrame(rafRef.current)
}
}
}, [])Smooth Scrolling
useEffect(() => {
if (wantsAutoScroll && listRef.current) {
listRef.current.scrollTo({
top: listRef.current.scrollHeight,
behavior: 'auto', // 'auto' prevents jank during streaming
})
}
}, [messages, wantsAutoScroll])Avoiding Unnecessary Rerenders
Split Context by Update Frequency
// Separate frequently-changing state from stable state
const ThemeContext = createContext<Theme>('light')
const ThemeActionsContext = createContext<ThemeActions>(null!)
// Components that only need actions don't rerender on theme change
function ThemeToggle() {
const { toggleTheme } = useContext(ThemeActionsContext)
return <button onClick={toggleTheme}>Toggle</button>
}Stable Callback References
// Bad - creates new function on every render
<Button onClick={() => handleClick(id)} />
// Good - stable reference
const handleItemClick = useCallback((id: string) => {
// handle click
}, [])
<Button onClick={() => handleItemClick(id)} />
// Best - if child is memoized
const handleItemClick = useCallback(() => {
handleClick(id)
}, [id, handleClick])
<MemoizedButton onClick={handleItemClick} />Bundle Size Optimization
Import Only What You Need
// Bad - imports entire library
import * as _ from 'lodash'
// Good - tree-shakeable imports
import debounce from 'lodash/debounce'
// Best - use native alternatives when possible
const debounce = (fn: Function, ms: number) => {
let timer: NodeJS.Timeout
return (...args: any[]) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), ms)
}
}Analyze Bundle
# Use rollup-plugin-visualizer (already in devDependencies)
nx build designer --mode=analyzeReact Security Best Practices
Guidelines for preventing XSS, injection attacks, and other security vulnerabilities.
Core Principles
1. Never trust user input - Sanitize all data from users, URLs, and external sources 2. Escape by default - React escapes JSX by default, but edge cases exist 3. Validate navigation state - URL params and location.state can be manipulated 4. Use allowlists - Prefer allowlists over blocklists for validation
XSS Prevention
React's Built-in Protection
React automatically escapes values in JSX:
// Safe - React escapes the content
const userInput = '<script>alert("xss")</script>'
return <div>{userInput}</div> // Renders as text, not HTMLdangerouslySetInnerHTML
Avoid unless absolutely necessary:
// DANGEROUS - only use with sanitized content
<div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />If you must use it, sanitize with a library like DOMPurify or rehype-sanitize.
Checklist Items
1. No Unsanitized dangerouslySetInnerHTML
| Attribute | Value |
|---|---|
| Description | dangerouslySetInnerHTML must use sanitized content |
| Search Pattern | grep -n "dangerouslySetInnerHTML" --include="*.tsx" |
| Pass Criteria | All uses wrapped with sanitization (DOMPurify, rehype-sanitize) |
| Severity | Critical |
| Fix | Sanitize with DOMPurify.sanitize(html) or use rehype-sanitize |
2. URL Parameters Are Validated
| Attribute | Value |
|---|---|
| Description | useParams values must be validated before use |
| Search Pattern | grep -A5 "useParams" --include="*.tsx" |
| Pass Criteria | Params validated with regex or allowlist before use |
| Severity | High |
| Fix | Validate with regex: /^[a-zA-Z0-9_-]+$/ |
3. Navigation State Is Validated
| Attribute | Value |
|---|---|
| Description | location.state can be manipulated and must be validated |
| Search Pattern | `grep -A10 "useLocation\ |
| Pass Criteria | State validated with type guards and default values |
| Severity | High |
| Fix | Use validation function like validateNavigationState() |
4. URLs Are Protocol-Validated
| Attribute | Value |
|---|---|
| Description | User-provided URLs must validate protocol (http/https only) |
| Search Pattern | `grep -E "new URL\( |
| Pass Criteria | URLs validated with isValidAndSafeURL or similar |
| Severity | Critical |
| Fix | Check ['http:', 'https:'].includes(url.protocol) |
5. User Content Has Length Limits
| Attribute | Value |
|---|---|
| Description | User input should have max length to prevent DoS |
| Search Pattern | `grep -E "maxLength\ |
| Pass Criteria | Text inputs have maxLength, displayed values truncated |
| Severity | Medium |
| Fix | Add maxLength to inputs, truncate display values |
6. Sensitive Data Not in URL
| Attribute | Value |
|---|---|
| Description | Tokens, passwords, API keys must not appear in URLs |
| Search Pattern | `grep -E "token=\ |
| Pass Criteria | No sensitive data in query params or path |
| Severity | Critical |
| Fix | Use headers or POST body for sensitive data |
7. External Links Use rel="noopener"
| Attribute | Value |
|---|---|
| Description | Links to external sites need noopener to prevent tabnabbing |
| Search Pattern | grep -E "target=[\"']_blank" --include="*.tsx" |
| Pass Criteria | All target="_blank" links have rel="noopener noreferrer" |
| Severity | Medium |
| Fix | Add rel="noopener noreferrer" to external links |
8. Form Actions Are Protected
| Attribute | Value |
|---|---|
| Description | Forms should prevent double submission and validate input |
| Search Pattern | `grep -E "<form |
| Pass Criteria | Forms disable submit during loading, validate before submit |
| Severity | Medium |
| Fix | Add disabled state during submission, validate inputs |
Sanitization Utilities
Config Value Sanitization
const MAX_CONFIG_VALUE_LENGTH = 100
export const sanitizeConfigValue = (value: unknown): string => {
if (!value) return 'Not set'
const str = String(value)
.replace(/[<>'"]/g, '') // Remove HTML/script injection characters
.trim()
return str.length > MAX_CONFIG_VALUE_LENGTH
? str.substring(0, MAX_CONFIG_VALUE_LENGTH) + '...'
: str
}URL Validation
export const isValidAndSafeURL = (urlString: string): boolean => {
try {
const url = new URL(urlString)
// Only allow http and https protocols
if (!['http:', 'https:'].includes(url.protocol)) {
return false
}
// Warn about localhost/private IPs in production
const hostname = url.hostname.toLowerCase()
const isLocalhost = hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname.startsWith('192.168.') ||
hostname.startsWith('10.') ||
hostname.startsWith('172.')
if (import.meta.env.PROD && isLocalhost) {
console.warn('Localhost/private IP detected in production:', hostname)
}
return true
} catch {
return false
}
}Navigation State Validation
export const validateNavigationState = (state: unknown): {
database: string
strategyName: string
strategyType: string
currentConfig: Record<string, any>
isDefault: boolean
} => {
const s = state as any
// Validate database name (alphanumeric and underscores only)
const database = typeof s?.database === 'string' &&
/^[a-zA-Z0-9_]+$/.test(s.database)
? s.database
: 'main_database'
// Validate strategy name (alphanumeric, hyphens, underscores)
const strategyName = typeof s?.strategyName === 'string' &&
/^[a-zA-Z0-9_-]+$/.test(s.strategyName)
? s.strategyName
: ''
// Validate against allowlist
const allowedTypes = ['BasicStrategy', 'AdvancedStrategy']
const strategyType = typeof s?.strategyType === 'string' &&
allowedTypes.includes(s.strategyType)
? s.strategyType
: 'BasicStrategy'
// Validate config is an object (contents still untrusted)
const currentConfig = s?.currentConfig &&
typeof s.currentConfig === 'object' &&
!Array.isArray(s.currentConfig)
? s.currentConfig
: {}
const isDefault = typeof s?.isDefault === 'boolean' ? s.isDefault : false
return { database, strategyName, strategyType, currentConfig, isDefault }
}Filter Key/Value Sanitization
const MAX_FILTER_KEY_LENGTH = 50
const MAX_FILTER_VALUE_LENGTH = 200
export const sanitizeFilterKey = (key: string): string => {
return key.replace(/[^a-zA-Z0-9_-]/g, '').substring(0, MAX_FILTER_KEY_LENGTH)
}
export const sanitizeFilterValue = (value: string): string => {
return value
.replace(/[<>'"\\]/g, '')
.trim()
.substring(0, MAX_FILTER_VALUE_LENGTH)
}Markdown Rendering
Use rehype-sanitize when rendering user-provided markdown:
import ReactMarkdown from 'react-markdown'
import rehypeSanitize from 'rehype-sanitize'
import remarkGfm from 'remark-gfm'
function SafeMarkdown({ content }: { content: string }) {
return (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeSanitize]}
>
{content}
</ReactMarkdown>
)
}Reserved Names
Prevent system name collisions:
export const RESERVED_NAMES = [
'default',
'null',
'undefined',
'none',
'system',
'admin',
'root',
'all',
'any',
]
export const validateName = (name: string): string | null => {
const trimmed = name.trim()
if (!trimmed) return 'Name is required'
if (RESERVED_NAMES.includes(trimmed.toLowerCase())) {
return `"${trimmed}" is a reserved name`
}
if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
return 'Cannot contain special characters'
}
if (trimmed.length > 100) {
return 'Name must be 100 characters or less'
}
return null
}Clipboard API Security
async function copyToClipboard(text: string) {
try {
// Sanitize before copying
const sanitized = text.replace(/[<>]/g, '')
await navigator.clipboard.writeText(sanitized)
return true
} catch (err) {
console.error('Failed to copy:', err)
return false
}
}State Management Patterns
Guidelines for managing state in React applications with TanStack Query and Context.
State Categories
| Type | Tool | Example |
|---|---|---|
| Server state | TanStack Query | API data, user profiles |
| Local UI state | useState | Form inputs, toggles |
| Shared UI state | Context | Theme, auth status |
| URL state | React Router | Filters, pagination |
| Form state | Controlled components | Input values, validation |
TanStack Query Patterns
QueryClient Configuration
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60_000, // 1 minute
gcTime: 5 * 60_000, // 5 minutes (was cacheTime)
retry: 2,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30_000),
refetchOnWindowFocus: false,
refetchOnMount: true,
refetchOnReconnect: true,
},
mutations: {
retry: 1,
},
},
})Query Key Factory Pattern
export const chatCompletionsKeys = {
all: ['chatCompletions'] as const,
completions: (namespace: string, projectId: string) =>
[...chatCompletionsKeys.all, namespace, projectId] as const,
session: (namespace: string, projectId: string, sessionId: string) =>
[...chatCompletionsKeys.completions(namespace, projectId), 'session', sessionId] as const,
}Checklist Items
1. Server State Uses TanStack Query
| Attribute | Value |
|---|---|
| Description | API data should use useQuery/useMutation, not useState+useEffect |
| Search Pattern | `grep -E "useState.*fetch\ |
| Pass Criteria | No manual fetch+setState patterns for server data |
| Severity | High |
| Fix | Convert to useQuery with queryFn |
2. Query Keys Use Factory Pattern
| Attribute | Value |
|---|---|
| Description | Query keys should use factory functions for consistency |
| Search Pattern | grep -E "queryKey:\s*\[" --include="*.ts" |
| Pass Criteria | Query keys reference factory (e.g., projectKeys.list(ns)) |
| Severity | Medium |
| Fix | Create key factory and reference it |
3. Mutations Invalidate Related Queries
| Attribute | Value |
|---|---|
| Description | Mutations should invalidate or update related query caches |
| Search Pattern | grep -A20 "useMutation" --include="*.ts" |
| Pass Criteria | onSuccess includes invalidateQueries or setQueryData |
| Severity | High |
| Fix | Add queryClient.invalidateQueries({ queryKey: ... }) in onSuccess |
4. Queries Have Proper enabled Condition
| Attribute | Value |
|---|---|
| Description | Queries with dependencies should use enabled option |
| Search Pattern | grep -A10 "useQuery" --include="*.ts" |
| Pass Criteria | Queries depending on params have enabled: !!param |
| Severity | Medium |
| Fix | Add enabled: !!dependency && !!otherDep |
5. Context Providers Have Proper Memoization
| Attribute | Value |
|---|---|
| Description | Context values should be memoized to prevent unnecessary rerenders |
| Search Pattern | grep -B5 -A15 "Provider value=" --include="*.tsx" |
| Pass Criteria | Context value uses useMemo or is a stable object |
| Severity | Medium |
| Fix | Wrap value with useMemo(() => ({ ... }), [deps]) |
6. Context Has Validation Hook
| Attribute | Value |
|---|---|
| Description | Context should have a custom hook that throws if used outside provider |
| Search Pattern | grep -A10 "useContext" --include="*.tsx" |
| Pass Criteria | Custom hook checks for undefined and throws descriptive error |
| Severity | High |
| Fix | Add if (!ctx) throw new Error('useX must be within XProvider') |
7. Avoid Prop Drilling
| Attribute | Value |
|---|---|
| Description | Props passed through 3+ levels should use Context |
| Search Pattern | Manual review of component hierarchies |
| Pass Criteria | No props passed unchanged through intermediate components |
| Severity | Medium |
| Fix | Create Context for deeply shared state |
8. Optimistic Updates for Mutations
| Attribute | Value |
|---|---|
| Description | User-facing mutations should use optimistic updates |
| Search Pattern | grep -A30 "useMutation" --include="*.ts" |
| Pass Criteria | Critical mutations have onMutate with optimistic update |
| Severity | Low |
| Fix | Add onMutate to update cache optimistically, onError to rollback |
Context Provider Pattern
type Theme = 'light' | 'dark'
interface ThemeContextType {
theme: Theme
toggleTheme: () => void
setTheme: (theme: Theme) => void
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
export function useTheme() {
const context = useContext(ThemeContext)
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider')
}
return context
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() => {
const saved = localStorage.getItem('theme') as Theme
if (saved) return saved
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
})
const setTheme = useCallback((newTheme: Theme) => {
setThemeState(newTheme)
localStorage.setItem('theme', newTheme)
}, [])
const toggleTheme = useCallback(() => {
setTheme(theme === 'light' ? 'dark' : 'light')
}, [theme, setTheme])
const value = useMemo(
() => ({ theme, toggleTheme, setTheme }),
[theme, toggleTheme, setTheme]
)
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
)
}Test QueryClient Pattern
function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
staleTime: 0,
},
mutations: {
retry: false,
},
},
})
}
function AllTheProviders({ children }: { children: React.ReactNode }) {
// Memoize to prevent recreation on re-renders
const [queryClient] = useState(() => createTestQueryClient())
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<BrowserRouter>{children}</BrowserRouter>
</ThemeProvider>
</QueryClientProvider>
)
}Combined Mutation Hooks
For related mutations, expose a unified interface:
export function useProjectMutations() {
const createMutation = useCreateProject()
const updateMutation = useUpdateProject()
const deleteMutation = useDeleteProject()
return {
create: createMutation,
update: updateMutation,
delete: deleteMutation,
isLoading:
createMutation.isPending ||
updateMutation.isPending ||
deleteMutation.isPending,
error:
createMutation.error ||
updateMutation.error ||
deleteMutation.error,
}
}