Matt Valley

Software Developer
Javascript (React, Node.js)

React.js: Maximise reusability of callback props with this simple technique

Callback Prop is the term I use to refer to a type of prop that meant to be invoked when something happens inside a child component. “onClick” of the standard button component is a good example of a callback prop. The callback will be invoked as soon as a user clicks on the button. The provided callback receives the related event as the first argument. Another example is the “onChange” prop of the “input” component which will be invoked as soon as the input value changes. The same pattern has been adapated by many React developers when developing new components. In this post, I will go through callback props in more details and what should be passed to them.

To begin, let’s develop a fancy button component with a standard onClick callback prop. Below is a simple implementation of this component:

import React from "react";
import PropTypes from "prop-types";

const FancyButton = (props) =>
   <button onClick={props.onClick}>{props.children}</button>

Button.propTypes = {
  onClick: PropTypes.func,
  children: PropTypes.element
};

export default FancyButton;

Let’s go ahead and use our fancy button to build a blog post form:

import React from "react";
import FancyButton from "./fancy-button";
import Form from "./form";
import TextBox from "./textbox";
import Label from "./label";

const BlogPostForm = () => {
  const onSaveAsDraft = () => {
    // save as draft
  };

   const onPublish = () => {
    // publish straight away
   }
 
  return (
    <Form>
       <Label>Title:</Label>
       <TextBox id="title" />
       <Label>Content:</Label>
       <TextBox id="content" multiline />
       <FancyButton type="outline" onClick={onSaveAsDraft}>
         Save as Draft
       </FancyButton>
       <FancyButton onClick={onPublish}>Publish</FancyButton>
    </Form>
  ); 
}
export default BlogPostForm;

As you can see in the above code, we have used the FancyButton in two places, for triggering “Save as Draft” as well as the “Publish” feature. We know that there is not much of difference between saving a post as a draft and publishing it straight away, it’s basically the value of the “draft” field that tells the back-end if the post should go live straight away or not. The problem with the above solution is that we have to define and maintain two different functions which are almost identical to each other.

We could use the Function.prototype.bind() function to use a single handler and pass the “draft” flag as an argument to the function, something like:

const onSave = (isDraft) => 
  // isDraft will be either true or false.

<FancyButton type="outline" onClick={onSave.bind(this, true)}>
  Save as Draft
</FancyButton>
<FancyButton onClick={onSave.bind(this, false)}>
  Publish
</FancyButton>

This kinda works but I’m not 100% happy with the solution. The problem is that we have to do this for every use case. Moreover, we’re not reusing the original onSave function but simply duplicating it. It might be OK for this case but what if we have to duplicate the callback 1000 times? Would that be OK as well? I don’t think so.

The way I usually fix this problem is by simply calling the callback with the provided props so let’s go ahead and refactor our FancyButton:

import React from "react";
import PropTypes from "prop-types";

const FancyButton = (props) => {
   const onClick = () => {
     props.onClick && props.onClick(props);
   }
   
   return <button onClick={onClick}>{props.children}</button>;
}

Button.propTypes = {
  onClick: PropTypes.func,
  children: PropTypes.element
};

export default FancyButton;

Next step for us would be introducing “draft” as a prop to the FancyButton! We don’t need to define it as a prop of the FancyButton but simply pass it to the FancyButton and get it back when the callback is invoked.

const onSave = ({ isDraft }) => 
  // isDraft will be either true or false.

<FancyButton isDraft type="outline"  onClick={onSave}>
  Save as Draft
</FancyButton>

<FancyButton isDraft={false} onClick={onSave}>
  Publish
</FancyButton>

No more binding. The beauty of this approach is in being reusable. “isDraft” is basically an example. You can simply attach any data to the FancyButton and get it back when the user clicks on the button. Think of a table that shows a list of user (say 100 of them) with an edit button next to each that says “Edit”! With this approach, we can have a single “onEditUser” callback and attach “userId” as a prop.

The technique of calling a callback with the provided props to a component is specially handly when building design systems and UI libraries using React. This technique has helped me so many times so far and it became kind of standard for me. Every time a component accepts a callback prop, I tend to invoke it with the provided props as the last argument.