Instruction file imported from dosandk/SkillStack (
.cursor/rules/react-client.mdc). Copyright stays with the author.
React best practices
Hooks
Hooks only at the top of a component or use* function. Components and hooks
at module level. Dependency arrays list every value the body reads.
// ❌ BAD
function ItemList({ items }: ItemListProps) {
return items.map(item => {
const [isSelected, setIsSelected] = useState(false);
function ItemBadge() {
return <Chip label={item.name} />;
}
return <ItemBadge />;
});
}
// ✅ GOOD
function ItemRow({ item }: ItemRowProps) {
const [isSelected, setIsSelected] = useState(false);
return <button onClick={() => setIsSelected(true)}>{item.name}</button>;
}
function ItemBadge({ name }: ItemBadgeProps) {
return <Chip label={name} />;
}
Render purity
Render is pure. No mutation, no ref.current I/O, no setState during render.
// ❌ BAD
function ItemCard({ item }: ItemCardProps) {
item.viewedAt = Date.now();
if (item.name === '') setIsEmpty(true);
return <Card>{item.name}</Card>;
}
// ✅ GOOD
function ItemCard({ item }: ItemCardProps) {
const isEmpty = item.name === '';
return <Card>{isEmpty ? 'Untitled' : item.name}</Card>;
}
Effects
Derive during render. Do not mirror props or session into state.
// ❌ BAD
useEffect(() => {
setItemLabel(item.name);
}, [item.name]);
useEffect(() => {
if (!user) {
setItems([]);
setError(null);
}
}, [user]);
// ✅ GOOD
const itemLabel = item.name;
const items = user ? storedItems : [];
const error = user ? storedError : null;
No setState in the synchronous effect body (including the start of an inner
async function). Init loading in useState; remount with key={id} to
refetch. Prefer async / await. Cleanup; .catch(), never void.
// ❌ BAD
useEffect(() => {
setIsLoading(true);
fetchItems(itemId).then(setItems);
}, [itemId]);
// ✅ GOOD
function ItemList({ itemId }: ItemListProps) {
const [items, setItems] = useState<Item[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
let isActive = true;
const loadItems = async () => {
try {
const result = await fetchItems(itemId);
if (isActive) setItems(result);
} catch (error: unknown) {
console.error(`Failed to load items for ${itemId}:`, error);
} finally {
if (isActive) setIsLoading(false);
}
};
loadItems().catch((error: unknown) => {
console.error(`Failed to load items for ${itemId}:`, error);
});
return () => {
isActive = false;
};
}, [itemId]);
if (isLoading) return <Skeleton />;
return <List>{/* items */}</List>;
}
<ItemList key={itemId} itemId={itemId} />
Module exports
UI files export named components only. Helpers live elsewhere. Context files may
also export useX.
// ❌ BAD
export const formatItemName = (name: string) => name.toLowerCase();
export function ItemRow({ item }: ItemRowProps) {
return <span>{formatItemName(item.name)}</span>;
}
// ✅ GOOD
export function ItemRow({ item }: ItemRowProps) {
return <span>{item.name}</span>;
}
export function AuthProvider({ children }: AuthProviderProps) {
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth(): AuthContextValue {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
Conditional JSX
Guard with a boolean. No nested ternaries.
// ❌ BAD
{itemCount && <Chip label={`${itemCount} items`} />}
{isLoading ? <Spinner /> : error ? <Alert /> : <List items={items} />}
// ✅ GOOD
{itemCount > 0 && <Chip label={`${itemCount} items`} />}
if (isLoading) return <Spinner />;
if (error) return <Alert />;
return <List items={items} />;
Checklist
- Hooks at the top of a component or
use*function; not nested. - Complete
useEffect/useCallback/useMemodependency arrays. - Render is pure: no mutation, no ref I/O, no setter in render.
- Derive instead of mirroring in an effect; no sync
setStateinuseEffect. - No deferred
setStateviaPromise.resolve,queueMicrotask, orsetTimeout. - Async effects clean up and attach
.catch()(nevervoid). - UI files export named components only; context files may also export
useX. - Conditional JSX guarded by a boolean; no nested ternaries.