Callback Naming
Why the on prefix matters and how to name event handler props so they read like English.
Value change handler
interface SliderProps {
value: number;
setValue: (v: number) => void;
}interface SliderProps {
value: number;
onValueChange: (value: number) => void;
}Child components don't "set" state — they signal that something changed. The on prefix followed by what happened (ValueChange) is the React convention. This mirrors native DOM events like onChange and onClick.
Delete action handler
interface TodoItemProps {
todo: Todo;
delete: () => void;
}interface TodoItemProps {
todo: Todo;
onDelete: () => void;
}onDelete clearly communicates this is an event callback. The component requests deletion — the parent performs it.
Form submission
interface SearchBarProps {
query: string;
search: (q: string) => void;
}interface SearchBarProps {
query: string;
onSearch: (query: string) => void;
}onSearch is an event callback that says "the user triggered a search." The parent handles the actual search logic.
Selection handler specificity
interface DropdownProps {
items: string[];
onChange: (item: string) => void;
}interface DropdownProps {
items: string[];
onItemSelect: (item: string) => void;
}onItemSelect tells you exactly what happened: the user selected an item. onChange is reserved for native <input> and <select> elements where it has established meaning.
For custom components, specific names like onItemSelect, onTabChange, or onColorPick describe the actual user action — invaluable when a component has multiple things that can change.
Close callback with reason
type CloseReason =
| 'backdropClick'
| 'escapeKeyDown'
| 'closeButton';
interface DialogProps {
children: React.ReactNode;
isOpen: boolean;
onClose: () => void;
/** Set after the dialog closes. */
lastCloseReason?: CloseReason;
}type CloseReason =
| 'backdropClick'
| 'escapeKeyDown'
| 'closeButton';
interface DialogProps {
children: React.ReactNode;
isOpen: boolean;
onClose: (reason: CloseReason) => void;
}Passing the reason as a callback parameter lets the parent decide how to respond to each close trigger in real time. For example, you might ignore backdrop clicks on a confirmation dialog but allow Escape.
MUI's Dialog uses exactly this pattern. A separate prop is reactive (updates after closing) instead of actionable (decides during closing).
Dual-level event callbacks
interface SliderProps {
min: number;
max: number;
value: number;
/** Fires on every value change. */
onChange: (value: number) => void;
/**
* Debounce delay in ms before onChange fires.
* Use 0 for real-time updates.
* @default 0
*/
changeDebounceMs?: number;
}interface SliderProps {
min: number;
max: number;
value: number;
/** Fires continuously as the thumb moves. */
onChange: (value: number) => void;
/** Fires once when the user releases the thumb. */
onChangeCommitted: (value: number) => void;
}Some interactions have two meaningful moments: the live update and the final commit. Two callbacks let the parent do different things at each moment — onChange for UI preview, onChangeCommitted for saving to the server.
MUI's Slider uses exactly this pattern. A changeDebounceMs config forces a trade-off between responsiveness and efficiency.
Drag and drop callbacks
interface DraggableProps {
children: React.ReactNode;
dragStart: () => void;
dragEnd: (pos: Position) => void;
drop: (target: string) => void;
}interface DraggableProps {
children: React.ReactNode;
onDragStart: () => void;
onDragEnd: (position: Position) => void;
onDrop: (targetId: string) => void;
}All three callbacks use the on prefix and descriptive parameter names (position vs pos, targetId vs target). Consistent naming across related events is key.