skills/react-use-callback/SKILL.md
Guides proper usage of the useCallback hook in React. Use this skill when optimizing function references, passing callbacks to memoized components, or preventing unnecessary re-renders.
npx skillsauth add Chris-Maskey/opencode-config react-use-callbackInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
useCallback caches a function definition between re-renders until its dependencies change.
Only use useCallback for specific performance optimizations - not by default.
When passing a function to a component wrapped in memo():
import { useCallback, memo } from 'react';
const ExpensiveChild = memo(function ExpensiveChild({ onClick }) {
// Expensive rendering logic
return <button onClick={onClick}>Click me</button>;
});
function Parent({ productId }) {
// Without useCallback, handleClick would be a new function every render
// causing ExpensiveChild to re-render unnecessarily
const handleClick = useCallback(() => {
console.log('Clicked:', productId);
}, [productId]);
return <ExpensiveChild onClick={handleClick} />;
}
When a function is used inside useEffect:
function ChatRoom({ roomId }) {
const createOptions = useCallback(() => {
return { serverUrl: 'https://localhost:1234', roomId };
}, [roomId]);
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]);
}
Better alternative: Move the function inside the effect:
function ChatRoom({ roomId }) {
useEffect(() => {
// Function defined inside effect - no useCallback needed
function createOptions() {
return { serverUrl: 'https://localhost:1234', roomId };
}
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
}
Always wrap functions returned from custom hooks:
function useRouter() {
const { dispatch } = useContext(RouterStateContext);
const navigate = useCallback((url) => {
dispatch({ type: 'navigate', url });
}, [dispatch]);
const goBack = useCallback(() => {
dispatch({ type: 'back' });
}, [dispatch]);
return { navigate, goBack };
}
Use updater functions to eliminate state dependencies:
// Before: todos is a dependency
const handleAddTodo = useCallback((text) => {
setTodos([...todos, { id: nextId++, text }]);
}, [todos]);
// After: No todos dependency needed
const handleAddTodo = useCallback((text) => {
setTodos(todos => [...todos, { id: nextId++, text }]);
}, []);
Without memo(), useCallback provides no benefit:
// useCallback is pointless here
function Parent() {
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
// Child will re-render anyway when Parent re-renders
return <Child onClick={handleClick} />;
}
Apps with page-level navigation don't benefit from memoization:
// Overkill for simple navigation
function App() {
const [page, setPage] = useState('home');
// Not needed - page transitions are inherently expensive anyway
const navigate = useCallback((page) => setPage(page), []);
return <Navigation onNavigate={navigate} />;
}
Accept JSX as children:
// Instead of memoizing onClick
function Panel({ children }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && children}
</div>
);
}
// Children don't re-render when Panel's state changes
<Panel>
<ExpensiveComponent />
</Panel>
Keep state local:
// Don't lift state higher than necessary
function SearchForm() {
// Local state doesn't trigger parent re-renders
const [query, setQuery] = useState('');
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
// Returns a new function every render
const handleClick = useCallback(() => {
doSomething();
}); // Missing dependency array!
// Correct
const handleClick = useCallback(() => {
doSomething();
}, []);
// Can't call hooks in loops
function List({ items }) {
return items.map(item => {
// WRONG
const handleClick = useCallback(() => sendReport(item), [item]);
return <Chart key={item.id} onClick={handleClick} />;
});
}
// Correct: Extract to component
function List({ items }) {
return items.map(item => (
<Report key={item.id} item={item} />
));
}
function Report({ item }) {
const handleClick = useCallback(() => sendReport(item), [item]);
return <Chart onClick={handleClick} />;
}
// Alternative: Wrap Report in memo instead
const Report = memo(function Report({ item }) {
function handleClick() {
sendReport(item);
}
return <Chart onClick={handleClick} />;
});
| Hook | Caches | Use Case |
|------|--------|----------|
| useCallback(fn, deps) | The function itself | Callback props |
| useMemo(() => fn, deps) | Result of calling function | Computed values |
// Equivalent
const memoizedFn = useCallback(fn, deps);
const memoizedFn = useMemo(() => fn, deps);
memo() wrapped childrenmemo() on child componentWhen memoization isn't working, debug dependencies:
const handleSubmit = useCallback((orderDetails) => {
// ...
}, [productId, referrer]);
console.log([productId, referrer]);
Check in browser console:
Object.is(temp1[0], temp2[0]); // First dependency same?
Object.is(temp1[1], temp2[1]); // Second dependency same?
React Compiler automatically memoizes values and functions, reducing the need for manual useCallback calls. Consider using the compiler to handle memoization automatically.
tools
Anti-patterns and mistakes to avoid as a product manager. Use when evaluating leadership behaviors, improving team dynamics, reflecting on management practices, or onboarding new product managers.
development
Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
testing
Design effective CTAs using visual attention and gaze psychology principles. Use when designing landing pages, button hierarchies, conversion elements, or optimizing user attention flow through interfaces.
tools
Run agent-browser + Chrome inside Vercel Sandbox microVMs for browser automation from any Vercel-deployed app. Use when the user needs browser automation in a Vercel app (Next.js, SvelteKit, Nuxt, Remix, Astro, etc.), wants to run headless Chrome without binary size limits, needs persistent browser sessions across commands, or wants ephemeral isolated browser environments. Triggers include "Vercel Sandbox browser", "microVM Chrome", "agent-browser in sandbox", "browser automation on Vercel", or any task requiring Chrome in a Vercel Sandbox.