DEV Community

Shannon Crabill
Shannon Crabill

Posted on • Originally published at shannoncrabill.com on

Abstracting Click Events in React

An essential part of programming is having clean and simplified code.

A function should do one thing, like update a follower count or submit a form, as opposed to multiple things at once. At the same time, if a function can be reused to perform similar actions based on what input or arguments it receives, it should do that.

As an example, let's say we have three buttons: "Pizza", "Cheeseburger" and "Ice Cream". We're feeling snackish, so let's assume that clicking a button, starts the order for that item.

Alt Text

If each Button was a component in React, the return statement of that component may look like this:

<button onClick={this.handleClick}>
  {this.props.text} // this.props.text = "Pizza", "Cheeseburger" or "Ice Cream"
</button>
Enter fullscreen mode Exit fullscreen mode

Which when a button is clicked, it runs a handleClick function.

handleClick = () => {
  this.props.orderFood(); // This could be anything
}
Enter fullscreen mode Exit fullscreen mode

While I could give each Button instance it's own handleClick function—handlePizzaOrder, handleCheeseBurgerOrder and handleIceCreamOrder—that is creating a lot of work, for functions that do very similar things: Order an item.

What we can do, is take what we know about the Button that was clicked and pass that information to a generalized or abstracted handleClick function. Then, based on that information, run a specific order function.

Abstracting onClick

To start, my Button component looked like this:

<button onClick={this.handleClick}>
  {this.props.text} // this.props.text = "Pizza", "Cheeseburger" or "Ice Cream"
</button>
Enter fullscreen mode Exit fullscreen mode

This means that when any of the three buttons are clicked, the handleClick function starts running. At this point, the handleClick function does not know which of the buttons was clicked. For this to happen, the handleClick function needs to take in the event target.

To do that, we'll change the onClick props to become an anonymous function that takes in the event.

The button goes from this:

<button onClick={this.handleClick}>
  {this.props.text} // this.props.text = "Pizza", "Cheeseburger" or "Ice Cream"
</button>
Enter fullscreen mode Exit fullscreen mode

To this:

<button onClick={(e) => this.handleClick(e)}>
  {this.props.text} // this.props.text = "Pizza", "Cheeseburger" or "Ice Cream"
</button>
Enter fullscreen mode Exit fullscreen mode

And the handleClick function is updated to accept the event as an argument.

handleClick = (e) => {
  // We'll update the rest of the function in the next step
}
Enter fullscreen mode Exit fullscreen mode

Running a Function, Based on the Event Target

Instead of an if/else statement, we can use a switch statement that looks at the innerText of the event target, and based on that, will fire a specific function.

For example, when the "Pizza" button is clicked, we want to start the pizza ordering process of picking a size and toppings. For "Cheeseburger" a number of burger patties, how they should be cooked, and toppings.

Here's our handleClick function and switch case:

handleClick = (e) => {
 switch (e.target.innerText) {
  case "Pizza":
    this.props.orderPizza();
    break;
  case "Cheeseburger":
    this.props.orderCheeseBurger();
    break;
  case "Ice Cream":
    this.props.orderIceCream();
    break;
  default:
    console.log("I'm sorry. We don't have that menu item.")
 }
}
Enter fullscreen mode Exit fullscreen mode

Now let's walk through what is happening.

If the “Pizza” button is clicked, an event object is passed to the function. It has a target property that returns this.

<button>Pizza</button>
Enter fullscreen mode Exit fullscreen mode

From there, we can get innerText which has a value of “Pizza”. Since that meets the first switch case, the orderPizza function is run. If the “Cheeseburger” button was clicked, the second case is fired, and so on.

If there was a fourth button that did not match any of the cases we have specified above, the default case is hit, which is our instance, prints a response to the console and exits the function.

Conclusion

With this setup, I can change what each switch case does, or add new ones without having to change the functionality of the buttons themselves. The return of the Button component stays minimal which makes it easy to read and maintain.

This example is in the context of React, but the principle behind abstracting and keeping components small and easy to maintain can apply to other programming languages.

What functions can you simply or combine in your projects?


While building my React & Redux project, I thought about how I can streamline how many functions and components I was using. An example being the game buttons.

The post Abstracting Button Events in React appeared first on Shannon Crabill — Software Engineer && Email Developer.

Top comments (8)

Collapse
 
chico1992 profile image
chico1992

Hi how would you handle internationalization with this for this for example Ice Cream in german is Eis and in french its Glace i don't know if making the switch logic on hardcoded values a good idea

btw
this

<button onClick={(e) => this.handleClick(e)}>
    {this.props.text} // this.props.text = "Pizza", "Cheeseburger" or "Ice Cream"
</button>

works the same as this:

<button onClick={this.handleClick}>
    {this.props.text} // this.props.text = "Pizza", "Cheeseburger" or "Ice Cream"
</button>
Collapse
 
thepeoplesbourgeois profile image
Josh

There are a surprising number of uncapitalized language names, among other grammatical errors, in this callout of the lack of i18n considerations. Also, while "ice" in French is indeed "glace" (not capitalized btw), "ice cream" is actually "crème glacée", and "Eis" (which, you're right, German writers would capitalize) refers just to "ice", not "ice cream".

Collapse
 
chico1992 profile image
chico1992

I know that "glace" in French is not capitalized, I wanted to go with the same format from the buttons.
I wanted to point out that the implementation is pretty fragile if the business decides one day that more languages need to be supported
something like the following would be safe ass the text will most likely be used as a i18n key depending on the library that is used

handleClick(itemName) {
    if(this.props[`order${itemName}`]===undefined){
        return ()=>{
            console.log("I'm sorry. We don't have that menu item.")
        };
    }
    return this.props[`order${itemName}`];
}
<button onClick={this.handleClick(this.props.text)}>
    {this.props.text} // this.props.text = "Pizza", "Cheeseburger" or "Ice Cream"
</button>
Collapse
 
r3tter profile image
Yaroslav

This way - onClick={this.handleClick} we also can get event object, coz it passes to function automatically. So there's no need for anonymous function. As you may know - the function of that type are bad for performance, the can trigger non expected re-render.

Collapse
 
pavermakov profile image
Pavel Ermakov

Sorry mate, but I'd rather have a separate function over a cluttered switch statement. Also, you are not reusing anything. You have to to update the switch statement every time you get a new button.

Collapse
 
thepeoplesbourgeois profile image
Josh

This apology seems rather insincere. If I were making it, I would provide some amount of helpful advice as to ways I saw for the code to improve, like "Instead of a switch statement (which you will need to add or remove cases to whenever the button behavior is updated), you'll likely find brace-notation access with an interpolated string to be a better abstraction.

handleClick(event) {
  const {currentTarget: {innerText: itemName}} = event;
  this.props[`order${itemName}`]();
}

Despite that, this is very solid, useful information! Sorry for not saying so earlier 😅"

Collapse
 
pavermakov profile image
Pavel Ermakov • Edited

My point was that the article did not convince me to use the listed approach. Going further, I do not recommend it at all, because the extra switch abstraction is pointless, makes code less readable and error-prone in a long run.

Thread Thread
 
serializator profile image
Julian

I'd argue that the logic of what to do with a specific type of order ("Pizza", "Cheeseburger" and "Ice Cream") shouldn't be the responsibility of the React component. It should be refactored outside of the component so that the component can pass some kind of metadata (the name such as "Pizza" etc) to the part of the system actually responsible for acting based on the business requirements (business logic).

If you want to go a step further into actually implementation details of the above example I'd say that reducers may be a good choice here so that the component dispatches an event and delegates the responsibility of acting upon that event (placing an order) to the reducer.

But... I'd also argue that this is an example and there are always more suitable solutions but which may not bring across the knowledge the way the author meant to.

In the example used I believe that the functions were kept in the same component and invoked the way they are based on a conditional to keep it small and digestible without scrolling from one example to the other to understand the relation between them.