Abstracting a reusable selectable input field

Chakra UI's Select component is a React component that allows a user to pick from a list of predefined options. It is extremely configurable but this often leads to a large, complex nested structure for even the simplest task.

Nesting related components within one another in this fashion is known as the compound component pattern and is a popular approach for most front-end libraries, with the keyword here being libraries. It allows for maximal flexibility at the cost of complexity.

From the application/consumer's perspective, it is often advisable to abstract this complexity away into a facade pattern, exposing only those aspects of the api that are relevant for the business logic of the app.

Consider the example of a simple, selectable field component that's used in a form. Here's how to do it purely using Chakra UI.

first the field needs to be wrapped in Field.Root

<Field.Root>...</Field.Root>

the content inside will be wrapped in Controller, which takes a render prop for defining how the field will be rendered (known as the "render props pattern")

<Field.Root>
    <Field.Label>...</Field.Label>
    <Controller
        ...
        render={({field}) => (...)}
    />
</Field.Root>

the field will be wrapped in Select.Root, along with all the other Select subcomponents necessary to get it working

<Field.Root>
    <Field.Label>...</Field.Label>
    <Controller
        ...
        render={({field}) => (
          <Select.Root
            name={field.name}
            onValueChange={({ value }) => field.onChange(value[0])}
            defaultValue={[field.value]}
            collection={collection}
          >
            <Select.HiddenSelect />
            <Select.Control>
              <Select.Trigger>
                <Select.ValueText placeholder={placeholder} />
              </Select.Trigger>
              <Select.IndicatorGroup>
                <Select.Indicator />
              </Select.IndicatorGroup>
            </Select.Control>
            <Select.Positioner>
              <Select.Content>
                {collection.items.map((item) => (
                  <Select.Item item={item} key={item.value}>
                    {item.label}
                    <Select.ItemIndicator />
                  </Select.Item>
                ))}
              </Select.Content>
            </Select.Positioner>
          </Select.Root>
        )}
    />
</Field.Root>

Having to perform these steps every single time the app needs a Select field is cumbersome. All of this complexity can easily be abstracted into a facade pattern that only exposes what's necessary for the app.

function SimpleSelectable({
  label,
  name,
  collection,
  placeholder,
  defaultValue,
  disable,
  control,
}: SimpleSelectableProps) {
  return (
    <Field.Root>
      <Field.Label>{label}</Field.Label>
      <Controller
        control={control}
        name={name}
        defaultValue={defaultValue}
        render={({ field }) => (
          <Select.Root
            name={field.name}
            disabled={disable ?? false}
            onValueChange={({ value }) => field.onChange(value[0])}
            defaultValue={[field.value]}
            collection={collection}
          >
            <Select.HiddenSelect />
            <Select.Control>
              <Select.Trigger>
                <Select.ValueText placeholder={placeholder} />
              </Select.Trigger>
              <Select.IndicatorGroup>
                <Select.Indicator />
              </Select.IndicatorGroup>
            </Select.Control>
            <Select.Positioner>
              <Select.Content>
                {collection.items.map((item) => (
                  <Select.Item item={item} key={item.value}>
                    {item.label}
                    <Select.ItemIndicator />
                  </Select.Item>
                ))}
              </Select.Content>
            </Select.Positioner>
          </Select.Root>
        )}
      />
    </Field.Root>
  );
}

Now, whenever a selectable field is required in the app, we invoke the SimpleSelectable component like this:

<SimpleSelectable
  disable={isDisabled}
  label={label}
  name={name}
  defaultValue={default}
  collection={items}
  control={control}
/>