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.
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…
// 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.
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
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:
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
dropdownsArray.map(renderAsDropDown);
And renderAsDropDown
will be:
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.
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
.
const dropdownsArr = constructDropdownArray(this.state, this.props);
In our constructDropdownArray
method, we do the following.
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.
// 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
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.
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.
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.
// 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!