DEV Community

simprl
simprl

Posted on • Edited on

What I want to say about Components composition in React

After 2 years of working with React I have some experience, which I would like to share. If you just started to learn React, then I hope this article will help you choose the right way of developing your project from 1–5 forms to a huge set of components and not to get confused.

If you are already a pro, then probably remember your faults. Or, perhaps, suggest better solutions to the described problems.
This article will talk about my personal opinion on how to organize the composition of the components.

Let’s start

Let’s consider some abstract form. We’ll assume that the form has many fields (about 10–15), but to keep your eyes open, let’s take a form with 4 fields as an example.

A multilevel object of this kind arrives at the component’s input:

const unit = {
  name: 'unit1',
  color: 'red',
  size: {
    width: 2,
    height: 4,
  },
}
Enter fullscreen mode Exit fullscreen mode

An inexperienced developer (like me in my first month of working with react) will do all this in a single component where the state will store the values of the inputs:

const Component = ({ values, onSave, onCancel }) => {
  const [ state, setState ] = useState({});

  useEffect(() => {
    setState(values);
  }, [ values, setState ]);

  return <div className="form-layout">
    <div className="form-field">
      <label>Name</label>
      <div className="input">
        <input onChange={({ target: { value } }) =>
          setState((state) => ({...state, name: value }))
        }/>
      </div>
    </div>
    <div className="form-field">
      <label>Color</label>
      <div className="input"> 
        <input onChange={({ target: { value } }) =>
          setState((state) => ({...state, color: value }))
        }/>
      </div>
    </div>
    <div className="size">
      <div className="block-label">Size</label>
      <div className="form-field">
        <label>Width</label>
        <div className="input">
          <input onChange={({ target: { value } }) =>
            setState((state) => ({...state, size: { width: value } }))
          }/>
        </div>
      </div>
      <div className="form-field">
        <label>Height</label>
        <div className="input">
          <input onChange={({ target: { value } }) =>
            setState((state) => ({...state, size: { height: value } }))
          }/>
        </div>
      </div>
    </div>
    <div className="buttons">
      <button onClick={() => onSave(state)}>Save</Button>
      <button onClick={() => onCancel()}>Cancel</Button>
    </div>
  </div>
}
Enter fullscreen mode Exit fullscreen mode

Seeing how quickly the developer coped, the customer will offer to make on the basis of this form one more, but that it was without the “size” block.

const unit = {
  name: 'unit1',
  color: 'red',
}
Enter fullscreen mode Exit fullscreen mode

And there are 2 options(both of them are wrong)

  1. You can copy the first component and add to it what’s missing or delete unnecessary things. This is usually happen when a component is not your own and you are afraid of breaking something in it.
  2. Add additional component settings to the parameters.

If after the implementation of 3–5 forms, the project is over, then the developer is lucky.

But this is usually just the beginning, and the number of different forms is only growing...

Then a similar one is needed, but without the “color” block.
Then a similar one, but with a new “description” block.
Then you need to make some blocks read-only.
Then a similar form must be inserted into another form — sometimes nothing good comes out of this

New forms by copying

A developer who chooses the copying approach will, of course, quickly cope with the implementation of new forms. As long as there are less than 10 of them. But then the mood will gradually fall.

Especially when redesign happens. The indentation between form blocks can be corrected "a little", the color selection component can be changed. After all, all at once you can not foresee, and many design decisions will have to reconsider after their implementation.

Here it is important to pay attention to the frequent reference to "similar form". After all, the product is one and all forms must be similar. As a result, you have to do a very uninteresting and routine work of redoing the same thing in each form, and testers by the way will also have to recheck each form.

In general, you get the point. Do not copy similar components.

New forms by generalization

If the developer chose the second way, then of course he’s on top of the game, you’ll think. He has only a few components that can draw dozens of forms. To fix the indentation throughout the project, or change the “color” component, you just need to fix two lines in the code and the tester will only have to check a couple of places.

But in fact this way created a very complex component.

It is difficult to use it, because there are a lot of parameters, some have almost the same name, to understand what is responsible for each parameter you have to get into the innards.

<Component
  isNameVisible={true}
  isNameDisabled={true}
  nameLabel="Model"
  nameType="input"
  isColorVisible={true}
  isColorDisabled={false}
  colorType={'dropdown'}
  isSizeVisible={true}
  isHeightVisible={true}
  isWidthDisabled={false}
/>
Enter fullscreen mode Exit fullscreen mode

It’s hard to maintain, too. As a rule, there are complex intertwined conditions inside and adding a new condition can break everything else. Tweaking a component to output one form can break all the others.

Anyway, you get the point. Don’t make a component with a lot of properties.

To solve the problems of the second option, developers start what? That’s right. Like real developers, they start developing something that simplifies the configuration of a complex component.

For example they make a parameter fields (like columns in react-table). And there they pass field parameters: which field is visible, which is not editable, field name.

The component call turns into this:

const FIELDS_CONFIG = {
    name: { visible: true, disabled: true, label: 'Model', type: 'input' },
    color: { visible: true, disabled: false, type: 'dropdown' },
    size: { visible: true },
    height: { visible: true },
    width: { disabled: false },
}
<Component
  values={values}
  fieldsConfig={FIELDS_CONFIG}
/>
Enter fullscreen mode Exit fullscreen mode

As a result, the developer is proud of himself. He generalized the settings of all the fields and optimized the internal code of the component: now one function is called for each field, which converts the configuration to the props of the corresponding component. Even the type name renders a different component. A little more and we will have our own framework.

How cool is that? Too much.

I hope it doesn’t turn into something like this:

const FIELDS_CONFIG = {
    name: getInputConfig({ visible: true, disabled: true, label: 'Model'}),
    color: getDropDownConfig({ visible: true, disabled: false}),
    size: getBlockConfig({ visible: true }),
    height: getInputNumberConfig({ visible: true }),
    width: getInputNumberConfig({ disabled: false }),
}
<Component
  values={values}
  fieldsConfig={FIELDS_CONFIG}
/>
Enter fullscreen mode Exit fullscreen mode

Anyway, you get the idea. Do not invent your custom code design

New forms by composing components and subforms

Let’s remember what we are writing on. We already have a React library. We don’t need to invent any new constructs. The configuration of components in react is described with JSX syntax.

const Form1 = ({ values }) => {
  return <FormPanel>
    <FormField disabled label=Model>
      <Input name="name" />
    </FormField>
    <FormField disabled label=Color>
      <DropDown name="color" />
    </FormField>
    <FormPanel>
      <FormField disabled label="Height">
        <Input.Number name="height" />
      </FormField>
      <FormField disabled label="Width">
        <Input.Number name="width" />
     </From Field>
    </FormPanelt>
  </FormPanel>
}
Enter fullscreen mode Exit fullscreen mode

It seems we are back to the first option with copying. But in fact we are not. This is a composition that gets rid of the problems of the first two approaches.

There is a set of bricks from which the form is assembled. Each brick is responsible for something special. Some are in charge of layout and appearance, some are in charge of data input.

If you need to change the indentation throughout the project, you can do this in the FormField component. If you need to change the work of a drop-down list, you can do it in one place in a component DropDown.

If you need a similar form, but, for example, so that there was no field “color”, then bring the common blocks in separate bricks and build another form.

Put the Size block into a separate component:

const Size = () =>  <FormPanel>
    <FormField disabled label="Height">
      <Input.Number name="height" />
    </FormField>
    <FormField disabled label=Width>
      <Input.Number name="width" />
   </From Field>
  </FormPanel>
Enter fullscreen mode Exit fullscreen mode

Make a form with a choice of colors:

const Form1 = () => <FormPanel>
    <FormField disabled label="Color">
      <DropDown name="color" />
   </FormField>
    <FormField disabled label="Model">
       <Input name="name" />
    </FormField>
    <Size name="size" />
</FormPanel>
Enter fullscreen mode Exit fullscreen mode

Then make a similar form, but without the choice of color:

const Form2 = () => <FormPanel>
    <FormField disabled label="Model">
       <Input name="name" />
    </FormField>
    <Size name="size" />
</FormPanel>
Enter fullscreen mode Exit fullscreen mode

Most importantly, the person who gets this code does not need to deal with the invented configs of the predecessor. Everything is written in JSX, familiar to any react-developer, with parameter hints for each component.

In general, you get the idea. Use JSX and component composition.

A few words about State

Now let’s turn our attention to the state. More precisely, his absence. Once we add the state, we lock the data flow and it becomes harder to reuse the component. All the bricks should be stateless (i.e. without the state). And only on the highest level can a form assembled from bricks be connected to the stack. If the form is complex, it already makes sense to divide it into multiple containers and connect each part to redux.

Don’t be lazy to make a separate component of the form. Then you can use it as part of another form, or build a statefull form on it, or a container to connect it to redux.
Of course, bricks can have internal state storages which are not related to the general data flow. For example, the internal state of DropDown is useful to store a flag of whether it’s expanded or not.

All in all, you get the idea. Separate the components into Stateless and Statefull

Total

Surprisingly, I periodically encounter all the errors described in the article and the problems that arise from them. I hope you won’t repeat them and then support of your code will become much easier.

I will repeat the main points:

  • Do not copy similar components. Use the DRY principle.
  • Don’t make components with a large number of properties and functionality. Each component must be responsible for something different (Single Responsibility from SOLID)
  • Separate components into Stateless and Statefull.
  • Don’t invent your own code constructions. Use JSX and composition of your components.

In fact, it was a preparatory article, so that the reader could better understand my further thoughts. After all, the main question remains unanswered. How to transfer data from one brick to another? Read about this in the next article.

Top comments (0)