home / skills / flpbalada / my-opencode-config / react-use-callback

react-use-callback skill

/skills/react-use-callback

This skill helps you optimize React performance by applying useCallback best practices to memoized components and effect dependencies.

npx playbooks add skill flpbalada/my-opencode-config --skill react-use-callback

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
6.6 KB
---
name: react-use-callback
description: 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.
---

# React: useCallback Best Practices

## Core Principle

**useCallback caches a function definition between re-renders until its dependencies change.**

Only use `useCallback` for specific performance optimizations - not by default.

## When to Use useCallback

### 1. Passing Callbacks to Memoized Children

When passing a function to a component wrapped in `memo()`:

```jsx
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} />;
}
```

### 2. Function as Effect Dependency

When a function is used inside `useEffect`:

```jsx
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:

```jsx
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]);
}
```

### 3. Custom Hook Return Values

Always wrap functions returned from custom hooks:

```jsx
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 };
}
```

### 4. Reducing State Dependencies

Use updater functions to eliminate state dependencies:

```jsx
// 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 }]);
}, []);
```

## When NOT to Use useCallback

### 1. Child Is Not Memoized

Without `memo()`, `useCallback` provides no benefit:

```jsx
// 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} />;
}
```

### 2. Coarse Interactions

Apps with page-level navigation don't benefit from memoization:

```jsx
// 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} />;
}
```

### 3. When Better Alternatives Exist

**Accept JSX as children:**

```jsx
// 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:**

```jsx
// 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)} />;
}
```

## Anti-Patterns to Avoid

### Missing Dependency Array

```jsx
// Returns a new function every render
const handleClick = useCallback(() => {
  doSomething();
}); // Missing dependency array!

// Correct
const handleClick = useCallback(() => {
  doSomething();
}, []);
```

### useCallback in Loops

```jsx
// 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} />;
});
```

## useCallback vs useMemo

| Hook | Caches | Use Case |
|------|--------|----------|
| `useCallback(fn, deps)` | The function itself | Callback props |
| `useMemo(() => fn, deps)` | Result of calling function | Computed values |

```jsx
// Equivalent
const memoizedFn = useCallback(fn, deps);
const memoizedFn = useMemo(() => fn, deps);
```

## Quick Reference

### DO

- Use with `memo()` wrapped children
- Use when function is an effect dependency
- Wrap custom hook return functions
- Use updater functions to reduce dependencies

### DON'T

- Add everywhere "just in case"
- Use without `memo()` on child component
- Use when you can restructure code instead
- Forget the dependency array

## Performance Debugging

When memoization isn't working, debug dependencies:

```jsx
const handleSubmit = useCallback((orderDetails) => {
  // ...
}, [productId, referrer]);

console.log([productId, referrer]);
```

Check in browser console:
```js
Object.is(temp1[0], temp2[0]); // First dependency same?
Object.is(temp1[1], temp2[1]); // Second dependency same?
```

## Future: React Compiler

React Compiler automatically memoizes values and functions, reducing the need for manual `useCallback` calls. Consider using the compiler to handle memoization automatically.

## References

- [React Docs - useCallback](https://react.dev/reference/react/useCallback)
- [React Docs - memo](https://react.dev/reference/react/memo)
- [React Docs - useMemo](https://react.dev/reference/react/useMemo)

Overview

This skill guides proper usage of React's useCallback hook to optimize function references and avoid unnecessary re-renders. It focuses on practical patterns, when useCallback helps, common anti-patterns, and safer alternatives for cleaner, faster components.

How this skill works

The skill inspects how functions are created and passed across component boundaries and recommends when to memoize them with useCallback. It highlights key scenarios: callbacks sent to memoized children, functions used as effect dependencies, and functions returned from custom hooks. It also points out cases where useCallback is unnecessary or harmful and suggests restructuring patterns.

When to use it

  • When passing a callback prop to a component wrapped in memo() to prevent needless re-renders.
  • When a function is listed as a dependency in useEffect and must remain stable between renders.
  • When returning functions from custom hooks so callers receive stable references.
  • When you can convert state updates to updater functions to remove state dependencies.

Best practices

  • Only use useCallback for targeted performance needs, not by default.
  • Always include a correct dependency array; missing dependencies produce new functions each render.
  • Prefer defining small helper functions inside effects instead of memoizing them when possible.
  • Wrap callback-returning custom hooks with useCallback to keep returned references stable.
  • Avoid calling hooks inside loops—extract per-item logic into a child component.

Example use cases

  • Parent component passes handleClick to an expensive child wrapped with memo() to avoid child re-renders.
  • Effect needs a stable helper: either memoize the helper or define it inside the effect to depend on specific values.
  • Custom routing hook returns navigate and goBack functions wrapped with useCallback to keep identity stable across renders.
  • Replacing direct state-dependent callbacks with updater-style setState to remove dependencies and simplify useCallback.

FAQ

Should I wrap every function with useCallback?

No. Only wrap functions when you need stable identity for memoized children, effect dependencies, or API consistency from hooks. Overusing it adds complexity without benefit.

What if my child component isn't memoized?

useCallback gives no benefit for non-memoized children because the child will re-render when the parent re-renders regardless of prop identity.