javascriptreactjsnext.jsreact-hooksheadless-ui

How to use render props in React to expose information about a component's state


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.


Solution

  • To be able to do

    <Switch>
          {({ checked }) => (
            ...
          )}
    </Switch>
    

    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 a Node, you would call children(...). So you could implement Switch with something like

    function Switch({children}){
      const [checked, setChecked] = useState(false);
      ...
    
      return (
        // it's of course more complex that that, but the core idea is here
        <div onClick={() => setChecked(current => !current)}> 
         {children({checked})}
        </div>
      );
    
    }
    

    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 uses utils/render.ts which extracts children from the props.