Skip to content

Martin Belev

How to keep cleaner React components with object map?

TypeScript, React2 min read

Programming

We will see one way to refactor our React components by replacing conditionals with object map. This is a favorite refactoring of mine because it makes the components easier to understand and extend, groups the logic in a single place, and requires fewer lines of code.


Simple conditionals example

A lot of times based on some input we want to display different information to our users. Let's see an example which should make things clearer:

1import React from 'react';
2
3interface Props {
4 errorField: 'name' | 'email' | 'password' | 'date';
5}
6
7const ErrorMessageWithSwitch: React.FC<Props> = ({ errorField }) => {
8 switch (errorField) {
9 case 'name':
10 return <div>Please enter valid name.</div>;
11 case 'email':
12 return <div>Please enter valid email address.</div>;
13 case 'password':
14 return <div>Please enter valid password.</div>;
15 case 'date':
16 return <div>Please enter valid date of birth.</div>;
17 default:
18 return <div>Invalid field.</div>;
19 }
20};
21
22const ErrorMessageWithIf: React.FC<Props> = ({ errorField }) => {
23 if (errorField === 'name') {
24 return <div>Please enter valid name.</div>;
25 }
26
27 if (errorField === 'email') {
28 return <div>Please enter valid email address.</div>;
29 }
30
31 if (errorField === 'password') {
32 return <div>Please enter valid password.</div>;
33 }
34
35 if (errorField === 'date') {
36 return <div>Please enter valid date of birth.</div>;
37 }
38
39 return <div>Invalid field.</div>;
40};

We have a component that should show the appropriate message for a certain errorField. It is a very simple one but by reading it we got some unpleasant feeling. A lot of writing and syntax which is making the code noisy and takes more time to go through. Not to speak about the fact that some tiny details can be missed as well.

By having a deeper look at the code, we can see that after all this is a simple mapping between one value to another. Doesn't this seem like a key/value structure? Here comes the object map, so let's see the refactored example:

1import React from 'react';
2
3interface Props {
4 errorField: 'name' | 'email' | 'password' | 'date';
5}
6
7const errorFields = {
8 name: 'Please enter valid name.',
9 email: 'Please enter valid email address.',
10 password: 'Please enter valid password.',
11 date: 'Please enter valid date of birth.',
12 default: 'Invalid field.'
13};
14
15const ErrorMessage: React.FC<Props> = ({ errorField }) => {
16 const message = errorFields[errorField] || errorFields.default;
17 return <div>{message}</div>;
18};

Such simple examples are easier to identify and start using object instead of if/switch. However, this kind of refactoring/technique is pretty powerful for a lot more complex cases where the benefits are bigger.

More complex example

Let's say we have a button component for connecting our account with Twitter.

1import React from "react";
2
3const ConnectTwitterButton: React.FC = () => {
4 const handleClick = () => {
5 // Connect Twitter
6 };
7
8 return (
9 <button onClick={handleClick}>
10 Connect with <TwitterIcon> Twitter
11 </button>
12 );
13};
14
15export default ConnectTwitterButton;

This is great, but now imagine we need to extend the current button's functionality to connect with more providers like Twitch/Facebook/.etc and we end up with something like:

1import React from 'react';
2
3interface Props {
4 providerType: 'twitter' | 'twitch' | 'fb';
5}
6
7const ConnectAccountButton: React.FC<Props> = ({ providerType }) => {
8 const getProviderName = () => {
9 switch (providerType) {
10 case 'twitter':
11 return 'Twitter';
12 case 'twitch':
13 return 'Twitch';
14 case 'fb':
15 return 'Facebook';
16 default:
17 return 'Unknown';
18 }
19 };
20 const getProviderIcon = () => {
21 // return SVG icon
22 };
23 const providerName = getProviderName();
24 const icon = getProviderIcon();
25
26 const connectWithTwitter = () => {
27 // Connect Twitter
28 };
29 const connectWithTwitch = () => {
30 // Connect Twitch
31 };
32 const connectWithFacebook = () => {
33 // Connect Facebook
34 };
35 const handleClick = () => {
36 if (providerType === 'twitter') {
37 return connectWithTwitter();
38 }
39 if (providerType === 'twitch') {
40 return connectWithTwitch();
41 }
42 if (providerType === 'fb') {
43 return connectWithFacebook();
44 }
45 };
46
47 return (
48 <button onClick={handleClick}>
49 Connect with {icon} {providerName}
50 </button>
51 );
52};
53
54export default ConnectAccountButton;

We have a couple of things per provider - name, icon and connect function. So what we can do about it?

1import React from 'react';
2
3type ProviderType = 'twitter' | 'twitch' | 'fb';
4
5interface Props {
6 providerType: ProviderType;
7}
8
9interface Provider {
10 icon: React.ReactNode;
11 name: string;
12 connect: () => void;
13}
14
15const providers: { [key in ProviderType]: Provider } = {
16 twitter: {
17 icon: <TwitterIcon />,
18 name: 'Twitter',
19 connect: connectWithTwitter
20 },
21 twitch: {
22 icon: <TwitchIcon />,
23 name: 'Twitch',
24 connect: connectWithTwitch
25 },
26 fb: {
27 icon: <FacebookIcon />,
28 name: 'Facebook',
29 connect: connectWithFacebook
30 }
31};
32
33const ConnectAccountButton: React.FC<Props> = ({ providerType }) => {
34 const { icon, name, connect } = providers[providerType];
35
36 return (
37 <button onClick={() => connect()}>
38 Connect with {icon} {name}
39 </button>
40 );
41};
42
43export default ConnectAccountButton;

The important part is the refactoring itself which is highlighted - line 15 to line 41. Now we look at our component it is very easy to understand what is happening and we have the logic centralized in a simple object.

Maybe it would be a bit harder to identify similar cases if you haven't done similar refactoring but ones you have done a couple it becomes easier and more obvious.

Bonus round using array

This one can be helpful with array usage as well. I think the most common example would be a navigation menu where the items are displayed based on some conditions - feature flags, simple checks based on the user data, .etc.

1const navigationItems = [
2 {
3 path: 'Nav 1',
4 visible: () => {
5 // Some visibility logic
6 }
7 },
8 {
9 path: 'Nav 2',
10 visible: () => {
11 // Some visibility logic
12 }
13 }
14];
15
16// Then we can simply use filter and map to construct our navigation
17navigationItems.filter((item) => item.visible()).map((item) => /* The mapped item */ item.path);

Conclusion

We saw a simple and a more complex example of how we can use object map and avoid using conditionals. Hopefully, the code looks much cleaner and easier to understand and extend for everyone.

A pattern can be noticed for cases where we can apply the object map usage - when we have data that is mapped to other data or behavior.

This is a refactoring that is not related specifically to React and JavaScript/TypeScript. It can be used in other languages as well when you see fit.

Of course, this is not a silver bullet and not every if/switch statements can be converted to such configuration object.

Thank you for reading this to the end 🙌 . If you enjoyed it and learned something new, support me by clicking the share button below to reach more people and/or give me a follow on Twitter where we can catch up. I am sharing some other tips, articles, and things I learn there.
If you don't like the article, please leave a comment below or write me a DM with feedback what you don't like so I can improve and provide better content in the future 💪.

© 2020 by Martin Belev. All rights reserved.