I have read about render props extensively on the official React documentation as well as other articles. However, I am trying to do something similar to what Tailwind does and am failing to figure out how they use this pattern in their components to expose state information about a component.
For example, if you have a look at their Switch component. Its usage is as follows:
function MyToggle() {
const [enabled, setEnabled] = useState(false)
return (
<Switch checked={enabled} onChange={setEnabled} as={Fragment}>
{({ checked }) => (
/* Use the `checked` state to conditionally style the button. */
<button
className={`${
checked ? 'bg-blue-600' : 'bg-gray-200'
} relative inline-flex h-6 w-11 items-center rounded-full`}
>
<span className="sr-only">Enable notifications</span>
<span
className={`${
checked ? 'translate-x-6' : 'translate-x-1'
} inline-block h-4 w-4 transform rounded-full bg-white transition`}
/>
</button>
)}
</Switch>
)
}
If I were to write the Switch component from scratch, I would instantiate a state for checked
in the component function but how do I make it so that the state is exposed when I use the component in the manner:
<Switch>
{({ checked }) => (
...
)}
</Switch>
From their github,they do something like:
function SwitchFn(props) {
let {
checked,
...theirProps
} = props
let [checked, onChange] = useControllable(controlledChecked, controlledOnChange, defaultChecked)
let slot = useMemo<SwitchRenderPropArg>(() => ({ checked }), [checked])
...
return (
<>
{name != null && checked && (
<Hidden
features={HiddenFeatures.Hidden}
{...compact({
as: 'input',
type: 'checkbox',
hidden: true,
readOnly: true,
form,
checked,
name,
value,
})}
/>
)}
{render({ ourProps, theirProps, slot, defaultTag: DEFAULT_SWITCH_TAG, name: 'Switch' })}
</>
)
}
But it is quite difficult to follow along. Please help. Thank you.
2
Answers
To be able to do
It means that
children
is a callback instead of a React Node.In the component implementation it means that instead of simply calling
children
that evaluates to aNode
, you would callchildren(...)
.So you could implement Switch with something like
It’s a specific case of a render prop, where the prop is
children
.Note
You could ask "But where is children actually called in that Tailwind code???"
switch.tsx
usesutils/render.ts
which extracts children from the props.Lets start with a perhaps simpler example, courtesy of Steven Wittens:
Here we wrap the text field to modify the default behavior. Note that it doesn’t have to be a
TextField
, and the value doesn’t have to be a number, that’s up to the user ofValidatingInput
.The important bit is that we ask the user of
ValidatingInput
to provide us with some stuff: the current state, a setter for that state, a parser to parse the input from the end user, and a formatter to show what gets displayed to the user in the input field.Then we provide the render prop function with the other half of the glue for wiring up to the DOM to respond to events. The definition of
ValidatingInput
could look something like this:Note that we get the "render" function from the normal
children
prop, and we pass it the stuff it needs. The internal state buffer is maintained but the value only flows to the outside viasetValue
if it parses. This is a nicer syntax for the old Higher-Order Component (HOC) approach to doing this same thing.Tailwind is doing basically the same thing to wrap the underlying DOM element here with their enhanced functionality and styling, it’s just that there is (as you’ve noticed) a lot more going on in their example because of the needs of a high-use production library codebase. Hopefully the simpler toy example above makes it clearer what’s happening.
Even this approach is falling out of favor compared to custom hooks, one could easily imagine a variation on the above that does something like