GuerillaRadio
GuerillaRadio

Reputation: 1297

Map through an array and render a specified component in React

I have 2 almost identical components that map through an array of options and generate either checkboxes or radio buttons. The only difference between the components is the child component that is rendered (either a checkbox or a radio input).

I would like to reduce the duplicate code in these components but I'm unsure as to the best way to tackle it. I could consolidate the 2 components into a single component such as FormControlGroup and add more props to allow me to select if I want either checkboxes or radio buttons to be rendered e.g. checkboxOrRadio, but this would mean that if I added new variations of checkboxes or radio buttons such as CustomCheckbox I would have to keep expanding on the props of this component.

<FormControlGroup
  label="Radio buttons"
  name="test"
  options=[{label: 'one', value: 1}, {label: 'two', value: 2}, {label: 'three', value: 3}]
  checkboxOrRadio="radio"
/>

Is it possible (and sensible) to pass a component in as a prop and have the component rendered in the map function? If so, how would I do this? Or are there any more elegant solutions? That way I could pass in any component I want and it will be rendered and have the key, name, label, onChange, value, checked props passed to it.

CheckboxGroup.js

import React from 'react';
import CheckboxInput from './CheckboxInput';

const CheckboxGroup = ({ label, name, options, onChange, children }) => {
  return (
    <div className="form-group form-inline">
      <span className="faux-label">{label}</span>

      {children}

      <div className="form-inline__field-container">
        {options &&
          options.map(option => (
            <CheckboxInput
              key={option.value}
              name={name}
              label={option.label}
              onChange={onChange}
              value={option.value}
              checked={option.value}
            />
          ))
        }
      </div>
    </div>
  );
};

export default CheckboxGroup;

RadioGroup.js

import React from 'react';
import RadioInput from './RadioInput';

const RadioGroup = ({ label, name, options, onChange, children }) => {
  return (
    <div className="form-group form-inline">
      <span className="faux-label">{label}</span>

      {children}

      <div className="form-inline__field-container">
        {options &&
          options.map(option => (
            <RadioInput
              key={option.value}
              name={name}
              label={option.label}
              onChange={onChange}
              value={option.value}
              checked={option.value}
            />
          ))
        }
      </div>
    </div>
  );
};

export default RadioGroup;

Upvotes: 1

Views: 2236

Answers (3)

ethan.roday
ethan.roday

Reputation: 2645

It is absolutely sensible in certain cases to pass a component as a prop. In fact, it is a pattern used in many React libraries, including React Router, which allows you to pass a component prop to the Route component.

In your case, the render function of your FormControlGroup component might look something like this:

render({component, ...}) { // component is a prop
  const InputComponent = component;
  return (
    ... // outer divs
    { options.map(option =>
      <InputComponent key={option.value} ... />
    }
    ...
  )
}

Then you would use it like this:

<FormControlGroup
  label="Radio buttons"
  name="test"
  options=[{label: 'one', value: 1}, {label: 'two', value: 2}, {label: 'three', value: 3}]
  component={CheckboxInput}
/>

Another option would be to create a new component that takes care of rendering the outer <div> elements and then map options to a list of input components in some outer component. That allows you to make fewer assumptions about what props any given input component should be expecting. Since you're already using children, you'd have to split it into two components. Here's one possibility for what that might look like:

const FormControlGroup = ({ label, children }) => {
  return (
    <div className="form-group form-inline">
      <span className="faux-label">{label}</span>
      {children}
    </div>
  );
};

const FormControlOptions = ({ children }) => {
  return (
    <div className="form-inline__field-container">
      {children}
    </div>
  );
};

const SomeOuterComponent = ({ label, name, options, onChange }) => {
  <FormControlGroup label={label}>
    ... // other children
    <FormControlOptions>
      {
        options.map(option =>
          <CheckboxInput
            key={option.value}
            name={name}
            label={option.label}
            onChange={onChange}
            value={option.value}
            checked={option.value}
          />
        )
      }
    </FormControlOptions>
  </FormControlGroup>
}

Of course, there are many ways to design this. The exact implementation you go with will depend on your use case.

Upvotes: 2

Bradey Whitlock
Bradey Whitlock

Reputation: 201

I personally wouldn't change it into a FormControlGroup component because it makes it simpler in the code where you're using the component. So you can quickly know what is a CheckboxGroup vs RadioGroup.

Changing it would mean checking the checkboxOrRadio prop every time to see exactly what component you were changing.

I'd say wait, if you start to get more elements you want the exact same way then create a common component. But for just two components to share some code it isn't a big deal.

Upvotes: 0

Theo Wittkovskiy
Theo Wittkovskiy

Reputation: 106

Doesn't seem like a bad idea to me. It's definitely possible, maybe it's not the most elegant solution, but I don't currently see what's wrong with it.

Upvotes: 0

Related Questions