About a year ago, I published
"How to give rendering control to users with prop getters".
In that post, I show the entire implementation (at the time) of
react-toggled
which I actually
built for the sole purpose of teaching some of the patterns that I used in
downshift
. It's a much smaller
and simpler component that implements many of the same patterns as downshift so
it served as a great way to teach the prop getters pattern.
Both react-toggled and downshift use the render prop pattern as a mechanism for React component logic code sharing. As I explained in my blog post "When to NOT use Render Props", that's the primary use case for the render prop pattern. But that's also the primary use case for React Hooks as well. And React Hooks are WAY simpler than class components + render props.
So does that mean that when React Hooks are stable you wont need render props at
all anymore? No! I can think of two scenarios where the render prop pattern
will still be very useful, and I'll share those with you in a moment. But let's
go ahead and establish my claim that hooks are simpler by comparing the current
version of react-toggled
with a hooks-based implementation.
If you're interested,
here's the current source for
react-toggled
.
Here's a typical usage of react-toggled
:
function App() {
return (
<Toggle>
{({ on, toggle }) => (
<button onClick={toggle}>{on ? 'on' : 'off'}</button>
)}
</Toggle>
)
}
If all we wanted was simple toggle functionality, our hook version would be:
function useToggle(initialOn = false) {
const [on, setOn] = useState(initialOn)
const toggle = () => setOn(!on)
return { on, toggle }
}
Then people could use that like so:
function App() {
const { on, toggle } = useToggle()
return <button onClick={toggle}>{on ? 'on' : 'off'}</button>
}
Cool! A lot simpler! But the Toggle component in react-toggled actually supports
a lot more. For one thing, it provides a helper called getTogglerProps
which
will give you the correct props you need for a toggler to work (including
aria
attributes for accessibility). So let's make that work:
// returns a function which calls all the given functions
const callAll =
(...fns) =>
(...args) =>
fns.forEach((fn) => fn && fn(...args))
function useToggle(initialOn = false) {
const [on, setOn] = useState(initialOn)
const toggle = () => setOn(!on)
const getTogglerProps = (props = {}) => ({
'aria-expanded': on,
tabIndex: 0,
...props,
onClick: callAll(props.onClick, toggle),
})
return {
on,
toggle,
getTogglerProps,
}
}
And now our useToggle
hook can use the getTogglerProps
:
function App() {
const { on, getTogglerProps } = useToggle()
return <button {...getTogglerProps()}>{on ? 'on' : 'off'}</button>
}
And it's more accessible and stuff. Neat right? Well, what if I don't need the
getTogglerProps
for my use case? Let's split this up a bit:
// returns a function which calls all the given functions
const callAll =
(...fns) =>
(...args) =>
fns.forEach((fn) => fn && fn(...args))
function useToggle(initialOn = false) {
const [on, setOn] = useState(initialOn)
const toggle = () => setOn(!on)
return { on, toggle }
}
function useToggleWithPropGetter(initialOn) {
const { on, toggle } = useToggle(initialOn)
const getTogglerProps = (props = {}) => ({
'aria-expanded': on,
tabIndex: 0,
...props,
onClick: callAll(props.onClick, toggle),
})
return { on, toggle, getTogglerProps }
}
And we could do the same thing to support the getInputTogglerProps
and
getElementTogglerProps
helpers that react-toggled
currently supports. This
would actually allow us to easily tree-shake out those extra utilities that our
app is not using, something that would be pretty unergonomic to do with a render
props solution (not impossible, just kinda ugly).
But Kent! I don't want to go and refactor all the places in my app that use the render prop API to use the new hooks API!!
Never fear! Check this out:
const Toggle = ({ children, ...props }) => children(useToggle(props))
There's your render prop component. You can use that just like you were using the old one and migrate over time. In fact, this is how I recommend testing custom hooks!
There's a little more to this (like how do we port the control props pattern to react hooks for example). I'm going to leave that to you to discover. Once you've tried it out for a little bit, then watch me do it. There's a catch with the way we've been testing things a bit that change slightly with hooks (thanks to JavaScript closures).
The remaining use case for render props
Ok, so we can refactor our components to use hooks, and even continue to export react components with a render prop-based API (you might be interested, you may even consider going all out with the hydra pattern). But let's imagine we're now in a future where we don't need render props for logic reuse and everyone's using hooks. Is there any reason to continue writing or using components that expose a render props API?
YES! Observe! Here's an example of using downshift with react-virtualized. Here's the relevant bit:
<List
// ... some props
rowRenderer={({ key, index, style }) => (
<div
// ... some props
/>
)}
/>
Checkout that rowRenderer
prop there. Do you know what that is? IT'S A RENDER
PROP! What!? 🙀 That's inversion of control in all its glory with render props
right there. That's a prop that react-virtualized
uses to delegate control of
rendering rows in a list to you the user of the component. If
react-virtualized
were to be rewritten to use hooks, maybe it could accept
the rowRenderer
as an argument to the useVirtualized
hook, but I don't
really see any benefit to that over it's current API. So I think render props
(and this style of inversion of control) are here to stay for use cases like
this.
Conclusion
I hope you find this interesting and helpful. Remember that React hooks are still in alpha and subject to change. They are also completely opt-in and will not require any breaking changes to React's API. I think that's a great thing. Don't go rewriting your apps! Refactor them (once hooks are stable)!
Good luck!