Prop Collections and Getters
July 8, 2021 •3 min read
When building a custom component, you might encounter a scenario where certain elements have identicle attributes or props applied to them.
Here's the Day&Night component we'll use as an example:
We want this component to have the following props and attributes:
aria-pressed
onClick
on
function useToggle() {
const [on, setOn] = React.useState(false);
const toggle = () => setOn(!on);
return {
on,
toggle,
};
}
const PropCollectionsAndGetters = () => {
const { toggle, on } = useToggle();
return (
<>
<Animation on={on}>
<Button on={on} onClick={toggle} aria-pressed={on} aria-label="toggle-button" />
</Animation>
<Switch on={on} onClick={toggle} aria-pressed={on} />
</>
);
};
We have common props for both the Button and Switch components. Both components require these props by default either for functionality or accessibility.
We can create a collection of props to share with both components, ensuring none of the required props are accidentally omitted.
Prop Collections
For common use cases, we can create prop collections containing all the required or default props for a component:
function useToggle() {
const [on, setOn] = React.useState(false);
const toggle = () => setOn(!on);
return {
on,
toggle,
toggleProps: {
on,
onClick: toggle,
"aria-pressed": on,
},
};
}
This enables us to add all default props to components more easily:
const PropCollectionsAndGetters = () => {
const { on, toggleProps } = useToggle();
return (
<Wrapper>
<Animation on={on}>
<Button {...toggleProps} aria-label="toggle-button" />
</Animation>
<Switch {...toggleProps} />
</Wrapper>
);
};
Prop Getters
Prop collections work well for a specific requirement, but they aren't very flexible.
We may want to pass in our own click handler for custom scenarios where an event is emitted onClick.
But the example below breaks the toggle functionality, because our own click handler is overriding the toggleProps
click handler we are also passing in.
const PropCollectionsAndGetters = () => {
const { on, toggleProps } = useToggle();
return (
<Wrapper>
<Animation on={on}>
<Button {...toggleProps} aria-label="toggle-button" />
</Animation>
<Switch
{...toggleProps}
onClick={() => console.log('hello')}
/>
</Wrapper>
);
};
The solution is to compose the default props we want to apply to the Button and Switch components with any custom props we also want to add.
getToggleProps
is a utility function that accepts any custom props or attributes we want to apply to an element or component and composes them with the default props.
function useToggle() {
const [on, setOn] = React.useState(false);
const toggle = () => setOn(!on);
const callAll = (...fns) => (...args) => fns.forEach(fn => fn?.(...args))
function getToggleProps({ onClick, ...props } = {}) {
return {
"aria-pressed": on,
onClick: callAll(onClick, toggle),
on,
...props,
};
}
return {
on,
toggle,
getToggleProps,
};
}
getToggleProps
returns an object of the composed custom and default props which we can spread to our components.
const PropCollectionsAndGetters = () => {
const { on, getToggleProps } = useToggle();
return (
<Wrapper>
<Animation on={on}>
<Button
{...getToggleProps({
"aria-label": "toggle-button",
onClick: () => console.log("hello"),
})}
/>
</Animation>
<Switch {...getToggleProps()} />
</Wrapper>
);
};
Further Reading
- The blog by Kent C. Dodds for this and other React patterns.