From frontend-experts
Reviews TypeScript and React code for security vulnerabilities (XSS, CSRF, eval), type safety issues (assertions, null handling), and error patterns. Auto-loads for TS/React reviews.
npx claudepluginhub jpoutrin/product-forge --plugin frontend-expertsThis skill uses the workspace's default tool permissions.
This skill provides TypeScript and React-specific code review guidelines. Use alongside `typescript-style` for comprehensive review.
Reviews React components for architecture, hooks usage, React 19 patterns, state management, performance optimization, accessibility, and TypeScript. Use before merging PRs, after new features, or for validation.
Systematically reviews code for security, performance, maintainability, error handling, testing, and accessibility issues with severity-ranked findings and specific fixes. Activates on review requests.
Validate TypeScript/React code against style and architectural conventions
Share bugs, ideas, or general feedback.
This skill provides TypeScript and React-specific code review guidelines. Use alongside typescript-style for comprehensive review.
// VULNERABLE - dangerouslySetInnerHTML without sanitization
<div dangerouslySetInnerHTML={{ __html: userInput }} />
// VULNERABLE - innerHTML assignment
element.innerHTML = userContent;
// SAFE - use DOMPurify
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />
// SAFER - avoid innerHTML entirely
<div>{userContent}</div> // React auto-escapes
// VULNERABLE - logging sensitive data
console.log("User data:", user); // May include tokens, passwords
console.log("Request:", request.headers); // Auth headers
// VULNERABLE - exposing in error messages
throw new Error(`Auth failed for ${credentials}`);
// SAFE - sanitize before logging
console.log("User ID:", user.id); // Only necessary fields
// VULNERABLE - code execution from strings
eval(userInput);
new Function(userInput)();
setTimeout(userInput, 1000); // String argument
// SAFE - avoid string evaluation
setTimeout(() => { processInput(userInput); }, 1000);
// VULNERABLE - no CSRF token
fetch('/api/delete', { method: 'POST', body: data });
// SAFE - include CSRF token
fetch('/api/delete', {
method: 'POST',
headers: { 'X-CSRF-Token': csrfToken },
body: data,
});
// DANGEROUS - bypasses type checking
const user = data as User;
const items = response as any[];
// SAFER - use type guards
function isUser(data: unknown): data is User {
return typeof data === 'object' && data !== null && 'id' in data;
}
if (isUser(data)) {
// data is now typed as User
}
// DANGEROUS - runtime error if null
const name = user!.profile!.name!;
// SAFE - explicit null handling
const name = user?.profile?.name ?? 'Unknown';
// Or with explicit checks
if (user?.profile?.name) {
const name = user.profile.name;
}
// BUG - unhandled promise rejection
async function fetchData() {
const response = await fetch(url);
return response.json(); // What if fetch fails?
}
// CORRECT - handle errors
async function fetchData() {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
} catch (error) {
console.error('Fetch failed:', error);
throw error; // Re-throw or return default
}
}
// BUG - fire and forget
saveUser(userData); // No await, no error handling
// CORRECT
await saveUser(userData);
// or
saveUser(userData).catch(console.error);
// BUG - stale closure, missing dependency
useEffect(() => {
fetchUser(userId); // userId not in deps!
}, []); // Empty deps = runs once with initial userId
// CORRECT - include all dependencies
useEffect(() => {
fetchUser(userId);
}, [userId]);
// BUG - memory leak, state update after unmount
useEffect(() => {
fetchData().then(setData);
}, []);
// CORRECT - cleanup with flag or AbortController
useEffect(() => {
let mounted = true;
fetchData().then((data) => {
if (mounted) setData(data);
});
return () => { mounted = false; };
}, []);
// BETTER - use AbortController
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]);
// BUG - using index as key (causes issues on reorder)
items.map((item, index) => <Item key={index} data={item} />)
// CORRECT - use stable unique identifier
items.map((item) => <Item key={item.id} data={item} />)
// PERFORMANCE - new function on every render
<Button onClick={() => handleClick(id)} />
// BETTER - memoize if passed to memoized child
const handleButtonClick = useCallback(() => handleClick(id), [id]);
<MemoizedButton onClick={handleButtonClick} />
// CODE SMELL - passing props through many levels
<App user={user}>
<Layout user={user}>
<Sidebar user={user}>
<UserProfile user={user} />
// BETTER - use Context for cross-cutting concerns
const UserContext = createContext<User | null>(null);
<UserContext.Provider value={user}>
<App />
</UserContext.Provider>
// SLOW - all items re-render when one changes
function ItemList({ items }: { items: Item[] }) {
return items.map(item => <ItemRow item={item} />);
}
// FAST - memoize individual items
const MemoizedItemRow = memo(ItemRow);
function ItemList({ items }: { items: Item[] }) {
return items.map(item => <MemoizedItemRow key={item.id} item={item} />);
}
// SLOW - recalculates on every render
function ExpensiveComponent({ data }: { data: number[] }) {
const sorted = [...data].sort((a, b) => a - b); // Every render!
const sum = data.reduce((a, b) => a + b, 0); // Every render!
return <div>{sorted.join(',')}: {sum}</div>;
}
// FAST - memoize expensive operations
function ExpensiveComponent({ data }: { data: number[] }) {
const sorted = useMemo(() => [...data].sort((a, b) => a - b), [data]);
const sum = useMemo(() => data.reduce((a, b) => a + b, 0), [data]);
return <div>{sorted.join(',')}: {sum}</div>;
}
// OVERHEAD - derived state should be computed
const [items, setItems] = useState<Item[]>([]);
const [itemCount, setItemCount] = useState(0); // Unnecessary!
// Update both when items change - easy to forget
setItems(newItems);
setItemCount(newItems.length); // Must keep in sync
// BETTER - derive from source of truth
const [items, setItems] = useState<Item[]>([]);
const itemCount = items.length; // Always in sync
// HEAVY - imports entire library
import _ from 'lodash';
import moment from 'moment'; // 300KB+
// LIGHT - import specific functions
import debounce from 'lodash/debounce';
import { format } from 'date-fns'; // Much smaller
// INACCESSIBLE - icon-only button without label
<button onClick={onClose}>
<CloseIcon />
</button>
// ACCESSIBLE
<button onClick={onClose} aria-label="Close dialog">
<CloseIcon />
</button>
// INACCESSIBLE - div with click handler
<div onClick={handleClick}>Click me</div>
// ACCESSIBLE - use button or add role/keyboard
<button onClick={handleClick}>Click me</button>
// or
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => e.key === 'Enter' && handleClick()}
>
Click me
</div>
// INACCESSIBLE - input without label
<input type="email" placeholder="Email" />
// ACCESSIBLE
<label>
Email
<input type="email" />
</label>
// or
<label htmlFor="email">Email</label>
<input id="email" type="email" />
// INACCESSIBLE - status only indicated by color
<span style={{ color: isError ? 'red' : 'green' }}>
{status}
</span>
// ACCESSIBLE - include text or icon
<span style={{ color: isError ? 'red' : 'green' }}>
{isError ? '❌ ' : '✓ '}{status}
</span>
// BUG - falsy check catches 0 and ''
if (!value) return; // Fails for value = 0 or ''
// CORRECT - explicit null/undefined check
if (value == null) return; // Catches null and undefined
// or
if (value === null || value === undefined) return;
// BUG - order matters, later overwrites earlier
const config = { ...defaultConfig, ...userConfig, timeout: 5000 };
// timeout is always 5000, even if userConfig.timeout was set!
// CORRECT - put defaults first
const config = { timeout: 5000, ...defaultConfig, ...userConfig };
// BUG - forEach doesn't return
const doubled = items.forEach(x => x * 2); // undefined!
// CORRECT - use map for transformation
const doubled = items.map(x => x * 2);
// BUG - filter callback should return boolean
const active = items.filter(item => item.status); // Works but unclear
// CORRECT - explicit boolean return
const active = items.filter(item => item.status === 'active');
Flag missing tests for:
// Missing test coverage examples
it('should show error message on fetch failure', async () => {});
it('should be keyboard navigable', () => {});
it('should handle empty item list', () => {});
it('should show loading state while fetching', () => {});
any types (use unknown + type guards)