Power of Eloquence

When saying “Hello World!” isn’t enough anymore

DRYING Your React Components - Combining by Using Arrays and ES6 Destructuring

| Comments

When building and writing up your React applications, you would typically be designing components that make up your UI screens for users to interact. Your components could be ranging from menu navigation tabs, tables displaying data, to paginated items, image gallery etc, etc..

You know.

The usual UI suspects full-stack web developers normally face.

For eg, let’s say if you were to up build a form that comes with some drop-down fields, your React code would look like this.

React App
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, { Component, Fragment } from "react";

export default class FormApp extends Component {
  state = {
     dropdown_shirt: null
  }
  searchMe = e => {
    // Magic is going to happen here.
  };

 pickMe = e => {
   //state your name!
   this.setState({[e.target.name]: e.target.value}
 }

  render() {
    return (
      <Fragment>
        <h1>Welcome to my Awesome React App</h1>
        <form className="form-container">
          <label htmlFor="dropdown_shirt">Shirts</label>
          <select name="dropdown_shirt" onChange={this.pickMe}>
            <option value="polo_tees">Polo Tees</option>
            <option value="sleeveless">Sleeveless</option>
            <option value="v_necks">V Necks</option>
          </select>
          <button onClick={this.searchMe}>Find me some tees!</button>
        </form>

        {/* The table data will be rendered here when searching */}
      </Fragment>
    );
  }
}

Nothing out of ordinary here.

A very typical React setup using local states, event handlers, JSX elements along with other useful React’s core APIs.

Then you continue adding other input fields such as checkboxes, radio buttons, text fields etc, etc.. to satisfy some user requirements behind them.

But…

As you obviously know, the more features you build, the more complex the app is going to become - especially at the code structure level.

What if we have a requirement such that not only, we have just one drop-down filter, but several more drop-down filters??

Perhaps with an extra 3 drop-down filters…

Give me more search options
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Pants dropdown field
<label htmlFor="dropdown_pants">Pants</label>
<select name="dropdown_pants" onChange={this.pickMe} value={this.state.dropdown_pants}>
  <option value="dress_pants">Dress pants</option>
  <option value="jeans">Jeans</option>
  <option value="baggy_pants">Baggy pants</option>
</select>

// Shoes dropdown field
<label htmlFor="dropdown_shoes">Shoes</label>
<select name="dropdown_shoes" onChange={this.pickMe} value={this.state.dropdown_shoes}>
  <option value="boots">Boots</option>
  <option value="sporty_shoes">Sporty shoes</option>
  <option value="leather_shoes">Leather shoes</option>
</select>

// Hats dropdown field
<label htmlFor="dropdown_hats">Hats</label>
<select name="dropdown_hats" onChange={this.pickMe} value={this.state.dropdown_hats}>
  <option value="beanie">Beanie</option>
  <option value="cowboy_hat">Cowboy hat</option>
  <option value="sports_cap">Sports Cap</option>
</select>

Great! So our search form gets funkier to have more drop-down search filters to choose from.

However, the problem emerges when our render function now takes in more drop-down components to render.

You may think it’s fine for a few components for now.

But, what if you decide to add more dropdown filters in the future? With that, your render function is going to get longer and longer such that your rendering section becomes one long poem to read!

Moreover, it is very repetitive and it is going to be hard in maintaining when the next developer comes in to extend/modify your once-so-called awesome form app, not to mention having to keep track of local states, functions, data props etc for each drop-down field.

Definitely not cool at all.

So what can we do to make this better with such repeated UI controls use?

🤔

Well. We can DRY them using arrays and destructuring.

To start off, you notice, in the previous examples, all the dropdown fields look identical to each other with the minor difference of their label names and their local states respectively. So, in my mental model, my array structure would be designed like this.

Array setup
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const dropdownsArr = [
    {
       label: "Shirts",
       name: "dropdown_shirt",
       value:  this.state.dropdown_shirt,
       options:  [
            {
                label: "Polo Tees"
                value: "polo_tees"
            },
            {
                label: "Sleeveless"
                value: "sleeveless"
            }
           // ...rest of options
       ]
    }
    ....
];

Knowing my current filter functionality as it stands, I was able to figure out the key attributes that make up for each drop-down filter. Each dropdown has a unique label name, a local state that keeps track of user-selected dropdown value along with an array of drop-down option values.

But in a real world, I won’t be maintaining that list of options for each drop-down field. These are better sourced from the external source like a database, CSV or external API provider which grants me this access. Let’s assume for the moment that the options property takes incoming data from some data API fetch. The same data will be stored as a prop so will get passed down to this component level. That prop name for such collection data is for eg called shirtsOptions.

Hence, our revised array structure will be

Array setup - revised
1
2
3
4
5
6
7
8
9
const dropdownsArr = [
  {
    label: "Shirts",
    name: "dropdown_shirt",
    value:  this.state.dropdown_shirt,
    options:  this.props.shirtsOptions
  }
  ....
];

Now it looks better and leaner. We can safely assume at this point our shirtsOptions uses our label and value properties for each item element to be part of the options array. So we’re good here.

Next, we add the other 3 drop-down fields, we get the following:

Array setup - continued
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const dropdownsArr = [
  {
    label: "Shirts",
    name: "dropdown_shirts",
    value: this.state.dropdown_shirts,
    options: this.props.shirtsOptions
  },
  {
    label: "Pants",
    name: "dropdown_pants",
    value: this.state.dropdown_pants,
    options: this.props.pantsOptions
  },
  {
    label: "Shoes",
    name: "dropdown_shoes",
    value: this.state.dropdown_shoes,
    options: this.props.shoesOptions
  },
  {
    label: "Hats",
    name: "dropdown_hats",
    value: this.state.dropdown_hats,
    options: this.props.hatsOptions
  }
];

From this, we can refactor our dropdowns rendering using map

Using map utility function
1
dropdownsArray.map(renderAsDropDown);

And renderAsDropDown will be:

Their UI rendering functions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
renderAsDropDown = ({label, name, value, options}) => {
  return (
    <Fragment>
      <label htmlFor={name}>{label}</label>
      <select name={name} onChange={this.pickMe} value={value}>
        {renderOptions(options)}
      </select>
    </Fragment>
  )
}

renderOptions = (options) => {
  return options.map( (option, index) => (
    <option value={option.value}>{option.label}</option>
  )
}

See what I have done here.

For my renderAsDropDown method, not only will I be iterating each dropdown item from the dropdownsArray within its callback method, but I also made use of ES6 object destructuring to bring its attributes out and map them to their correct JSX props placement which makes up my dropdown component along with its children components such as the options.

That’s it!

That’s how you can DRY our your repetitive UI control code using such splendid ES6 features for this purpose. 🤟🤘🤟🤘🤟.

Awesome!

But what if I could tell you that we could take this a step further?

Let’s say that you look at the following dropdownArr construction.

Hello again…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const dropdownsArr = [
  {
    label: "Shirts",
    name: "dropdown_shirts",
    value: this.state.dropdown_shirts,
    options: this.props.shirtsOptions
  },
  {
    label: "Pants",
    name: "dropdown_pants",
    value: this.state.dropdown_pants,
    options: this.props.pantsOptions
  },
  {
    label: "Shoes",
    name: "dropdown_shoes",
    value: this.state.dropdown_shoes,
    options: this.props.shoesOptions
  },
  {
    label: "Hats",
    name: "dropdown_hats",
    value: this.state.dropdown_hats,
    options: this.props.hatsOptions
  }
];

While this looks alright on the surface, we can still refactor this even more by decoupling out state and props.

Abstracted to its own function method
1
const dropdownsArr = constructDropdownArray(this.state, this.props);

In our constructDropdownArray method, we do the following.

Array construction method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const constructDropdownArray = ({
    dropdown_shirts,
    dropdown_pants,
    dropdown_shoes,
    dropdown_hats
  },{
    shirtsOptions,
    pantsOptions,
    shoesOptions,
    hatsOptions
  }) => {
  ....

  return const array = [
      {
          label: "Shirts",
          name: "dropdown_shirts",
          value:  dropdown_shirts,
          options:  shirtsOptions
      },
      {
          label: "Pants",
          name: "dropdown_pants",
          value:  dropdown_pants,
          options:  pantsOptions
      },
      {
          label: "Shoes",
          name: "dropdown_shoes",
          value:  dropdown_shoes,
          options:  shoesOptions
      },
      {
          label: "Hats",
          name: "dropdown_hats",
          value:  dropdown_hats,
          options:  hatsOptions
      }
  ];
}

Again, using our ES6 object destructuring, we can do the same destructuring strategy for state and props as they’re also POJOs as per my previous array object example.

This is all very cool.

But what if, along the way, you want to modify some extra behaviours like our existing onChange event as each dropdown has different onchange event requirements now? Thus we need to cater this change for our array construction. How should we do this?

Simple.

We append another attribute to each item object of the array

In our constructDropdownArray method, we do the following.

A little tweaks here
1
2
3
4
5
6
7
8
9
10
11
// adding changFn property
 array = [
  {
    label: "Shirts",
    name: "dropdown_shirts",
    changeFn: someEventHandler
    value:  dropdown_shirts,
    options:  shirtsOptions
  },
  ....
];

Yup. You can even pass JS functions to the arrays as well.

To use this updated structure, if we go back to our React component

Adding event handler functions to the component class
1
2
3
4
5
6
7
8
export default class FormApp extends Component {
  // provided event handlers
  onShirtSelectedClick = () => {};
  onPantsSelectedClick = () => {};
  onShoesSelectedClick = () => {};
  onHatsSelectedClick = () => {};
  .....
}

In the same component, within our render function, we tweak this constructDropdownArray function to add an extra parameter.

1
const dropdownsArr = constructDropdownArray(this, this.state, this.props);

Yup. You heard me.

I’m passing this to the calling function.

So I can do this.

Destructuring ‘this’ context
1
2
3
4
5
6
7
8
9
10
const constructDropdownArray = (
    {
        onShirtsSelectedChange,
        onPantsSelectedChange,
        onShoesSelectedChange,
        onHatsSelectedChange,
    }, ...rest ) => {
    ....
    ....
}

Using object destructure , I can just grab any existing properties or functions on my current react component and then slab them into my item array in their respective drop-down item’s changeFn property.

Therefore, in our renderAsDropdown, we do the simple tweak to have the following.

Array setup
1
2
3
4
5
6
7
8
9
10
11
// add another attribute to the destructure signature
renderAsDropDown = ({ label, name, value, changeFn, options }) => {
  return (
    <Fragment>
      <label htmlFor={name}>{label}</label>
      <select name={name} onChange={changeFn} value={value}>
        {renderOptions(options)}
      </select>
    </Fragment>
  );
};

What’s amazing about this setup is that the Eventlistener function call still works at the renderAsDropdown point because we didn’t lose the current binding context of this event after it’s been destructured already.

Amazing isn’t it??

I was pretty stoked myself when I discovered how object destructure can also be used for this type of situation when building my form UIs.

Tricks like these are indeed useful to help to build your UI components to scale greatly.

Hope you learnt something useful.

Till then, Happy Coding!

Comments