skip to Main Content

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


  1. 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.

    Login or Signup to reply.
  2. Lets start with a perhaps simpler example, courtesy of Steven Wittens:

    <ValidatingInput
      parse={parseNumber} format={formatNumber}
      value={number}    setValue={setNumber}
    >{
      (value, isError, onChange, onBlur) =>
        <TextField
          value={value}      isError={isError}
          onChange={onChange} onBlur={onBlur} />
    }</ValidatingInput>
    

    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 of ValidatingInput.

    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:

    const ValidatingInput = ({
      value,
      setValue,
      parse = x => x,
      format = x => x,
      children: render,
    }) => {
      const [internalValue, setInternalValue] = useState(value);
      const [isError, setError] = useState();
      const onChange = useCallback((evt) => {
        setInternalValue(evt.target.value);
      }, []);
    
      const onBlur = useCallback(() => {
        try {
          setValue(parse(internalValue));
          // clear any error state after successful parse
          setError();
        } catch (err) {
          setError(err);
        }
      }, []);
    
      return render({ isError, onChange, onBlur, value: format(internalValue) });
    };
    

    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 via setValue 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

    const { onBlur, onChange, isError, currentValue } = useValidation({ value, setValue });
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search