The article was originally posted on my personal blog.
Developers often find themselves in situations where they need to return different result based on various conditions. One specific case where this happens often is when we want to render different JSX inside component based on some state variable that can be toggled.
As a result, oftentimes the code ends up looking like this:
const DataCard = ({ data }) => {
const [cardType, setCardType] = useState("sessions");
const Icon = cardType === "sessions" ? IconSession : IconPost;
const title = cardType === "sessions" ? "Daily user sessions" : "Post data";
return (
<div className="data-card">
<Icon />
<Button
onClick={() =>
setCardType(type => (type === "sessions" ? "post" : "sessions"))
}
>
Switch view
</Button>
<h2 className="data-card__title">{title}</h2>
{data[cardType].map(item => (
<div className="data-card__data">
<p>{item.name}</p>
<p>{item.data}</p>
</div>
))}
</div>
);
};
Here's a simple example where we have a data card, as a part of some analytics dashboard, with predefined styles and layout. The card allows switching between sessions
and post
data. The only elements that are changing are the card icon and title, so it makes sense to introduce cardType
boolean, based on which the appropriate icon and title are rendered. Additionally the data of correct type will be displayed based on this toggle.
Apart from the code being repetitive, there's another issue with such approach. Let's imagine that our component now has an additional data type to display - pageViews
. At this point we need to refactor the toggle button into a dropdown of available types as a first step. Next we might introduce a switch
statement instead of verbose if/else
conditions. As a result, the updated component will look as follows:
const DataCard = ({ data }) => {
const [cardType, setCardType] = useState({
value: "sessions",
label: "Sessions"
});
let Icon, title;
switch (cardType.value) {
case "sessions":
Icon = IconSession;
title = "Daily user sessions";
break;
case "post":
Icon = IconPost;
title = "Post data";
break;
case "pageViews":
Icon = IconPage;
title = "Page views";
break;
default:
throw Error(`Unknown card type: ${cardType}`);
}
return (
<div className="data-card">
<Icon />
<Dropdown
options={[
{ value: "sessions", label: "Sessions" },
{ value: "post", label: "Posts" },
{ value: "pageViews", label: "Page Views" }
]}
onChange={selected => setCardType(selected)}
/>
<h2 className="data-card__title">{title}</h2>
{data[cardType.value].map(item => (
<div className="data-card__data">
<p>{item.name}</p>
<p>{item.data}</p>
</div>
))}
</div>
);
};
The code looks a lot less repetitive and in case we need to display more types of data it's quite easy to add new case
and an option to the dropdown. However, we can still do better. What if we could get title
and Icon
from some sort of configuration object depending on the value of dataType
? Sounds like we need a sort of mapping between the data types and component variables. This is where we could use Map
data structure.
Map is ES6 addition and is simply a collection of key-value pairs. Historically in JS objects were used for storing dictionaries of such pairs, however Map has a few advantages over objects:
1. Map keeps the order of the keys by their insertion, which is not the case for the objects, where the order is not guaranteed.
2. Map can have any value as its key, whereas for objects it's only strings and symbols.
3. Map can be directly iterated whereas objects in most cases require some sort of transformations before that (e.g. with Object.keys
, Object.values
or Object.entries
).
4. Similarly the size of Map can be easily determined using size
prop. The object has to be transformed into array using one of the methods mentioned above.
5. Map has certain performance benefits in cases of frequent addition/removal operations.
Now that we're familiar with maps, let's refactor our component to take advantage of this data structure.
const typeMap = new Map([
["sessions", ["Daily user sessions", IconSession]],
["post", ["Post data", IconPost]],
["pageViews", [" Page views", IconPage]]
]);
const DataCard = ({ data }) => {
const [cardType, setCardType] = useState({
value: "sessions",
label: "Sessions"
});
const [title, Icon] = typeMap.get(cardType.value);
return (
<div className="data-card">
<Icon />
<Dropdown
options={[
{ value: "sessions", label: "Sessions" },
{ value: "post", label: "Posts" },
{ value: "pageViews", label: "Page Views" }
]}
onChange={selected => setCardType(selected)}
/>
<h2 className="data-card__title">{title}</h2>
{data[cardType.value].map(item => (
<div className="data-card__data">
<p>{item.name}</p>
<p>{item.data}</p>
</div>
))}
</div>
);
};
Notice how much leaner the component has become after refactoring switch
into a Map. At first the Map might seem a bit weird, looking like a multidimensional array. The first element is the key and second one is the value. Since keys and values can be anything, we map our data types to arrays, where the first element is title and the second one is the icon component. Normally getting those two values out of this nested array would be a bit of work, however destructuring assignment syntax makes it an easy task. Additional benefit of this syntax is that we can name our variables anything, which is handy in case we want to rename title
or Icon
into something else, without modifying the Map itself. The Map is declared outside of the component so it doesn't get unnecessarily re-created on every render.
While we're at it, why not refactor the array of dropdown options into a Map as well? The options are just mappings between values and labels, a perfect use case for a Map!
const typeMap = new Map([
["sessions", ["Daily user sessions", IconSession]],
["post", ["Post data", IconPost]],
["pageViews", [" Page views", IconPage]]
]);
const typeOptions = new Map([
["sessions", "Sessions"],
["post", "Posts"],
["pageViews", "Page Views"]
]);
const DataCard = ({ data }) => {
const [cardType, setCardType] = useState({
value: "sessions",
label: "Sessions"
});
const [Icon, title] = typeMap.get(cardType.value);
return (
<div className="data-card">
<Icon />
<Dropdown
options={[...typeOptions].map(([value, label]) => ({ value, label }))}
onChange={selected => setCardType(selected)}
/>
<h2 className="data-card__title">{title}</h2>
{data[cardType.value].map(item => (
<div className="data-card__data">
<p>{item.name}</p>
<p>{item.data}</p>
</div>
))}
</div>
);
};
Since Map does not have map
method, it needs to be transformed into array first. This can be done by using array spread or Array.from. Here again we benefit from destructuring assignment so we can easily access label
and value
inside the map method's callback and then create an object with those keys and their values.
The end result looks pretty lean and maintainable, where we only need to make a few changes to our maps in case more date types are added.
Top comments (6)
:) Hey! What about Object.entries()?
Info:
developer.mozilla.org/ru/docs/Web/...
caniuse.com/#search=object.entries
Yep, that works as well :)
PS: Considerable con - Map is non-serializable by JSON.stringify by default.
Haven't thought about that, but a really good point indeed!
Hi Alex,
Thanks for this short pro Map, but I don't understand why you can access the Map value element using cardType [Object] only (like this in your code)
const [title, Icon] = typeMap.get(cardType)
and not using his value prop ?
const [title, Icon] = typeMap.get(cardType.value)
Hi Benoit,
Thank you for the comment. It actually should be
const [title, Icon] = typeMap.get(cardType.value)
, I've forgot to update it after introducing the dropdown. I'll fix it in the article.Thank you for pointing it out!