Skip to main content

React Prop Getters

Introduction

Prop getters are a powerful advanced React pattern that helps you create flexible, reusable components with clean, intuitive APIs. This pattern is particularly useful when building component libraries or complex UI components that need to be highly customizable while maintaining a consistent interface.

In essence, prop getters are functions that return an object of props that can be spread onto DOM elements or other React components. They encapsulate common behavior and properties, making your components more composable and easier to use.

Understanding the Problem

Before diving into prop getters, let's understand the problem they solve. Consider this simple toggle component:

jsx
function Toggle({ onChange }) {
const [on, setOn] = React.useState(false);

const handleClick = () => {
setOn(!on);
onChange && onChange(!on);
};

return (
<div>
<button onClick={handleClick}>
{on ? 'ON' : 'OFF'}
</button>
</div>
);
}

This works fine, but what if users want to add their own click handlers or other props to the button? We'd need to expose every possible prop and handle combining them with our internal logic:

jsx
function Toggle({ onChange, onClick, ...buttonProps }) {
const [on, setOn] = React.useState(false);

const handleClick = (event) => {
setOn(!on);
onChange && onChange(!on);
onClick && onClick(event);
};

return (
<div>
<button onClick={handleClick} {...buttonProps}>
{on ? 'ON' : 'OFF'}
</button>
</div>
);
}

This approach quickly becomes unwieldy as the number of elements and customizable props increases. Prop getters offer a cleaner solution.

Implementing Prop Getters

The essence of prop getters is to create functions that return an object of props that can be spread onto elements. Here's how to implement the toggle component using prop getters:

jsx
function useToggle(initialOn = false) {
const [on, setOn] = React.useState(initialOn);

const toggle = () => setOn(!on);

const getTogglerProps = ({ onClick, ...props } = {}) => {
return {
'aria-pressed': on,
onClick: (event) => {
toggle();
onClick && onClick(event);
},
...props,
};
};

return {
on,
toggle,
getTogglerProps,
};
}

Now we can use this hook in our component:

jsx
function Toggle({ onChange }) {
const { on, getTogglerProps } = useToggle();

React.useEffect(() => {
onChange && onChange(on);
}, [on, onChange]);

return (
<div>
<button {...getTogglerProps()}>
{on ? 'ON' : 'OFF'}
</button>
</div>
);
}

And users of our component can easily add their own functionality:

jsx
function App() {
return (
<Toggle
onChange={(on) => console.log('Toggle changed to', on)}
/>
);
}

// Or with custom button behavior
function CustomToggle() {
const { on, getTogglerProps } = useToggle();

return (
<button
{...getTogglerProps({
onClick: () => console.log('Custom click handler'),
className: 'custom-button',
})}
>
{on ? 'ACTIVE' : 'INACTIVE'}
</button>
);
}

How Prop Getters Work

Let's break down the key aspects of prop getters:

  1. Function Creation: A prop getter is a function that returns an object of props.
  2. Accepting External Props: It accepts an optional object of props that the consumer wants to apply.
  3. Merging Logic: It merges its internal props with any props passed by the consumer.
  4. Special Prop Handling: For certain props like event handlers, it takes special care to call both the internal and external handlers.

Advanced Prop Getter Techniques

Handling Multiple Event Handlers

When working with multiple event handlers, it's important to ensure all handlers are called correctly:

jsx
function callAll(...fns) {
return (...args) => {
fns.forEach(fn => fn && fn(...args));
};
}

function getTogglerProps({ onClick, ...props } = {}) {
return {
'aria-pressed': on,
onClick: callAll(toggle, onClick),
...props,
};
}

The callAll utility ensures all functions get called with the same arguments without the need for nested function calls.

Multiple Prop Getters

Complex components often need multiple prop getters for different elements:

jsx
function useCustomSelect(items, initialSelectedIndex = 0) {
const [selectedIndex, setSelectedIndex] = React.useState(initialSelectedIndex);
const [isOpen, setIsOpen] = React.useState(false);

function getToggleButtonProps({ onClick, ...props } = {}) {
return {
'aria-haspopup': true,
'aria-expanded': isOpen,
onClick: callAll(() => setIsOpen(!isOpen), onClick),
...props,
};
}

function getItemProps({ onClick, index, ...props } = {}) {
return {
role: 'menuitem',
'aria-selected': selectedIndex === index,
onClick: callAll(() => {
setSelectedIndex(index);
setIsOpen(false);
}, onClick),
...props,
};
}

return {
selectedItem: items[selectedIndex],
isOpen,
getToggleButtonProps,
getItemProps,
};
}

This approach gives consumers complete flexibility while maintaining the component's internal logic.

Real-World Example: Building a Custom Dropdown

Let's create a complete dropdown component using prop getters:

jsx
function useDropdown(items = [], initialSelectedIndex = 0) {
const [selectedIndex, setSelectedIndex] = React.useState(initialSelectedIndex);
const [isOpen, setIsOpen] = React.useState(false);
const dropdownRef = React.useRef(null);

// Close dropdown when clicking outside
React.useEffect(() => {
if (!isOpen) return;

const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
};

document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);

function getDropdownProps(props = {}) {
return {
ref: dropdownRef,
...props,
};
}

function getToggleButtonProps({ onClick, ...props } = {}) {
return {
'aria-haspopup': true,
'aria-expanded': isOpen,
onClick: callAll(() => setIsOpen(!isOpen), onClick),
...props,
};
}

function getMenuProps(props = {}) {
return {
role: 'menu',
'aria-hidden': !isOpen,
style: { display: isOpen ? 'block' : 'none' },
...props,
};
}

function getItemProps({ onClick, index, ...props } = {}) {
return {
role: 'menuitem',
'aria-selected': selectedIndex === index,
onClick: callAll(() => {
setSelectedIndex(index);
setIsOpen(false);
}, onClick),
...props,
};
}

return {
selectedItem: items[selectedIndex],
isOpen,
getDropdownProps,
getToggleButtonProps,
getMenuProps,
getItemProps,
};
}

// Helper function to call multiple event handlers
function callAll(...fns) {
return (...args) => {
fns.forEach(fn => fn && fn(...args));
};
}

Now, using this hook in a component:

jsx
function Dropdown({ items, onChange }) {
const {
selectedItem,
isOpen,
getDropdownProps,
getToggleButtonProps,
getMenuProps,
getItemProps,
} = useDropdown(items);

React.useEffect(() => {
if (onChange) {
onChange(selectedItem);
}
}, [selectedItem, onChange]);

return (
<div {...getDropdownProps({ className: 'dropdown' })}>
<button
{...getToggleButtonProps({ className: 'dropdown-toggle' })}
>
{selectedItem || 'Select an item'}
</button>

<ul {...getMenuProps({ className: 'dropdown-menu' })}>
{items.map((item, index) => (
<li
key={index}
{...getItemProps({
index,
className: `dropdown-item ${selectedItem === item ? 'selected' : ''}`,
})}
>
{item}
</li>
))}
</ul>
</div>
);
}

This component can now be used with minimal props for basic functionality or extensively customized:

jsx
// Basic usage
<Dropdown
items={['Apple', 'Banana', 'Cherry']}
onChange={(item) => console.log(`Selected: ${item}`)}
/>

// Customized usage
function CustomDropdown() {
const items = ['Small', 'Medium', 'Large'];
const {
getDropdownProps,
getToggleButtonProps,
getMenuProps,
getItemProps,
} = useDropdown(items);

return (
<div
{...getDropdownProps({
className: 'size-selector',
'data-testid': 'size-dropdown'
})}
>
<button
{...getToggleButtonProps({
className: 'fancy-button',
onClick: () => console.log('Toggle clicked')
})}
>
Select Size
</button>

<div {...getMenuProps({ className: 'floating-menu' })}>
{items.map((size, index) => (
<div
key={size}
{...getItemProps({
index,
className: 'size-option',
onClick: () => console.log(`Size ${size} selected`)
})}
>
{size}
</div>
))}
</div>
</div>
);
}

Benefits of Prop Getters

  1. Separation of Concerns: Separate the component's logic from its presentation.
  2. Flexibility: Allow consumers to customize the component extensively.
  3. Clean API: Provide an intuitive and consistent interface.
  4. Reusability: Create components that are easier to reuse in different contexts.
  5. Accessibility: Ensure proper accessibility attributes are always applied.

Common Pitfalls and Best Practices

Pitfalls to Avoid

  1. Overriding Important Props: Be careful about which props can be overridden.
  2. Performance Concerns: Creating new functions on each render can impact performance.
  3. Complex Interfaces: Too many prop getters can make your API confusing.

Best Practices

  1. Use TypeScript: Define clear types for your prop getters to improve developer experience.
  2. Document Your API: Clearly document what props each getter provides and what can be overridden.
  3. Memoize Functions: Use React.useCallback to memoize your prop getter functions.
  4. Prioritize Accessibility: Always include appropriate ARIA attributes in your prop getters.
jsx
// Memoized prop getters
const getToggleButtonProps = React.useCallback(
({ onClick, ...props } = {}) => ({
'aria-pressed': on,
onClick: callAll(toggle, onClick),
...props,
}),
[on, toggle]
);

Summary

Prop getters are a powerful pattern for building flexible and reusable React components. They provide a clean way to encapsulate component logic while giving consumers the freedom to customize behavior and appearance. By returning objects of props that can be spread onto DOM elements, prop getters create an intuitive API that balances internal functionality with external customization.

Key takeaways:

  • Prop getters are functions that return objects of props
  • They allow merging of internal logic with consumer customizations
  • They solve the problem of prop drilling and API complexity
  • They're particularly useful for UI component libraries
  • Using helper functions like callAll() simplifies handling multiple event handlers

Additional Resources

Practice Exercises

  1. Basic Toggle: Implement a simple toggle button using prop getters.
  2. Tabs Component: Build a tabs component using multiple prop getters for tab buttons and panels.
  3. Form Controls: Create form input components with prop getters that handle validation and state.

Further Reading

  • Kent C. Dodds' articles on component patterns
  • React hooks documentation for understanding state management
  • ARIA guidelines for building accessible components

Remember that prop getters are just one of many patterns for building flexible components. The best approach depends on your specific requirements and the complexity of your components.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)