useCallback returns a memoized version of the callback that only changes if one of the dependencies has changed. This is useful for passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders.
Signature
function useCallback<T extends Function>(
callback: T,
inputs: ReadonlyArray<unknown>
): T
Parameters
callback
T extends Function
required
The callback function to memoize. This function will be returned on subsequent renders if the dependencies haven’t changed.
inputs
ReadonlyArray<unknown>
required
An array of dependencies. The callback will only be recreated if one of these values changes (compared using ===).
Returns
Returns the memoized callback function of type T. On the initial render, it returns the passed callback. On subsequent renders, it returns the same callback reference if dependencies haven’t changed, or a new callback if they have.
Basic Usage
import { useCallback, useState } from 'preact/hooks';
import { memo } from 'preact/compat';
const Button = memo(({ onClick, children }) => {
console.log('Button rendered');
return <button onClick={onClick}>{children}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [otherState, setOtherState] = useState(0);
// Memoize callback to prevent Button re-renders
const increment = useCallback(() => {
setCount(c => c + 1);
}, []); // No dependencies, never recreated
return (
<div>
<p>Count: {count}</p>
<Button onClick={increment}>Increment</Button>
<button onClick={() => setOtherState(s => s + 1)}>
Other State: {otherState}
</button>
</div>
);
}
With Dependencies
function SearchResults({ query, category }) {
const [results, setResults] = useState([]);
// Recreate callback when query or category changes
const handleSearch = useCallback(async () => {
const data = await fetch(`/api/search?q=${query}&cat=${category}`);
const json = await data.json();
setResults(json);
}, [query, category]);
return (
<div>
<button onClick={handleSearch}>Search</button>
<ResultsList results={results} />
</div>
);
}
Passing to Child Components
const TodoItem = memo(({ todo, onToggle, onDelete }) => {
console.log('TodoItem rendered:', todo.id);
return (
<div>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</div>
);
});
function TodoList() {
const [todos, setTodos] = useState([]);
const handleToggle = useCallback((id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
}, []);
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
return (
<div>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</div>
);
}
Event Handlers
function Form({ userId, onSubmit }) {
const [formData, setFormData] = useState({ name: '', email: '' });
const handleChange = useCallback((field) => {
return (e) => {
setFormData(prev => ({
...prev,
[field]: e.target.value
}));
};
}, []);
const handleSubmit = useCallback((e) => {
e.preventDefault();
onSubmit({ ...formData, userId });
}, [formData, userId, onSubmit]);
return (
<form onSubmit={handleSubmit}>
<input value={formData.name} onChange={handleChange('name')} />
<input value={formData.email} onChange={handleChange('email')} />
<button type="submit">Submit</button>
</form>
);
}
With useEffect
function DataFetcher({ url, params }) {
const [data, setData] = useState(null);
const fetchData = useCallback(async () => {
const query = new URLSearchParams(params).toString();
const response = await fetch(`${url}?${query}`);
const json = await response.json();
setData(json);
}, [url, params]);
useEffect(() => {
fetchData();
}, [fetchData]); // Safe to use as dependency
return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}
Debounced Callbacks
function SearchInput({ onSearch }) {
const [query, setQuery] = useState('');
const timeoutRef = useRef(null);
const debouncedSearch = useCallback((value) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
onSearch(value);
}, 500);
}, [onSearch]);
const handleChange = useCallback((e) => {
const value = e.target.value;
setQuery(value);
debouncedSearch(value);
}, [debouncedSearch]);
return <input value={query} onChange={handleChange} />;
}
Callback Factories
function ItemList({ items, onItemClick }) {
// Create memoized callback factory
const createClickHandler = useCallback((item) => {
return () => onItemClick(item);
}, [onItemClick]);
return (
<ul>
{items.map(item => (
<li key={item.id}>
<button onClick={createClickHandler(item)}>
{item.name}
</button>
</li>
))}
</ul>
);
}
With Context
const ActionsContext = createContext();
function ActionsProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
// Memoize all action creators
const actions = useMemo(() => ({
addItem: (item) => dispatch({ type: 'ADD_ITEM', payload: item }),
removeItem: (id) => dispatch({ type: 'REMOVE_ITEM', payload: id }),
updateItem: (item) => dispatch({ type: 'UPDATE_ITEM', payload: item })
}), []);
return (
<ActionsContext.Provider value={actions}>
{children}
</ActionsContext.Provider>
);
}
function Component() {
const { addItem } = useContext(ActionsContext);
// Safe to use in effects/callbacks
const handleAdd = useCallback(() => {
addItem({ id: Date.now(), text: 'New item' });
}, [addItem]);
return <button onClick={handleAdd}>Add Item</button>;
}
Relationship with useMemo
// These are equivalent:
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
const memoizedCallback = useMemo(() => {
return () => {
doSomething(a, b);
};
}, [a, b]);
useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). It’s a convenience hook specifically for memoizing functions.
Use useCallback when passing callbacks to optimized child components (wrapped in memo) that rely on reference equality to prevent unnecessary renders.
Don’t use useCallback everywhere. Only use it when:
- Passing callbacks to memoized child components
- Using the callback as a dependency in other hooks like
useEffect
- The callback is expensive to create