DEV Community

Peter Vera
Peter Vera

Posted on

Why you should use pure components

I've seen a number of articles explaining what pure components are and tutorials about how to write them. I haven't seen as many good justifications for why you should consider structuring your components as pure components first. I'm hoping to make a good case for them.

Impure Components Tend To Inhibit Use Cases

If you bundle a components state and behavior with its presentation layer, you risk obstructing important use cases.

As an example, let's say this minimal React toggle that maintains its own state is part of the component library your team uses:

// Bear with me here.
const Toggle = (props) => {
  const [isOn, setIsOn] = React.useState(props.initialState);

  const handleToggle = () => {
    setIsOn(!isOn);
    props.onToggle(isOn);
  };
  return (<button onClick={handleToggle}>{`${isOn ? "on" : "off"}`}</button>);
}
Enter fullscreen mode Exit fullscreen mode

What are the features of this toggle?

  1. You can set an initial states
  2. It maintains its own state
  3. It informs you when the state changes

Then, let's say you're working on a UI that's going to let your user toggle a setting that might be costly. Your design team wants to make sure people aren't going to turn it on by mistake, so they want you to insert a confirmation before actually making the switch to the on state.

Rough mockup for a toggle with a confirmation modal

This toggle implementation actually won't support this use case. There isn't a place to insert a dialog confirmation before switching the state of the toggle to on.

That toggle might be a little too contrived, so let's take a look at a real world component that was designed before declarative UIs caught on: dijit/form/ValidationTextBox from version 1.10 of the Dojo Toolkit.

Screenshot from the ValidationTextBox docs

It's your standard text box, with some functionality that performs validation and displays valid states. I've copied some of its relevant parameter documentation here:

Parameter Type Description
required boolean User is required to enter data into this field.
invalidMessage string The message to display if value is invalid.
missingMessage string The message to display if value is empty and the field is required.
pattern string This defines the regular expression used to validate the input.

You can see that they've tried to supply functionality to support a simple required prop to test if the text box contains a value, and a pattern prop to validate the text box's value with regular expressions.

Now, what sorts of use cases do these props not support?

  1. Validation based on external values, e.g., is this value already present in a list of values you've entered prior?
  2. Server-side validation, e.g. is this username taken?

In order to support #1, ValidationTextBox also allows you to override the validator function in a subclass, but if you look into the source you'll find that the output of validator is used synchronously, meaning asynchronous validation, as in #2, might be impossible. As an aside, overriding validator means the required and pattern props will be ignored unless you explicitly use them in your custom validator.

Instead, imagine it exposed the property isValid, which would trigger valid or invalid styling. I'd bet you could deliver the equivalent functionality in less time than it would take you to even understand the current API, and could support those additional use cases.

You Can Ship Those Behaviors On Top Anyway

Let's say you are convinced and rewrite your toggle component to be pure.

const PureToggle = (props) => {
  return (<button onClick={() => props.handleClick()}>
    {`${props.isOn ? "on" : "off"}`}
  </button>);
}
Enter fullscreen mode Exit fullscreen mode

But you don't want to throw away your hard work and you really want your consumers to not have to write those behaviors themselves. That's fine! You can also release those behaviors, in many forms including...

Pure functions

const toggle = (previousState) => {
  return !previousState;
}
Enter fullscreen mode Exit fullscreen mode

Hooks

const useToggle = (initialState = false) => {
  const [isOn, setIsOn] = useState(initialState);
  return [isOn, () => {
/
    const nextValue = toggle(isOn);
    setIsOn(nextValue);
    return nextValue
  }];
};
Enter fullscreen mode Exit fullscreen mode

Or Even A Higher Order Component!

const ToggleComponentWithBehavior = (props) => {
  const [isOn, doToggle] = useToggle(props.initialState);
  return (<PureToggle
    isOn={isOn}
    handleClick={() => {
      const nextValue = doToggle();
      props.onToggle(nextValue);
    }
  }/>);
};
Enter fullscreen mode Exit fullscreen mode

You might have noticed, but that higher order component actually exposes the exact same API as the original, behavior-coupled toggle implementation. If that's your ideal API, you can still ship it, and shipping the pure version will support the use cases you've missed.

Takeaways

Now you might be thinking "OK, but I'm not writing a component library, I'm writing a product. The components I write have specific-case behavior so this doesn't apply to me." The underlying concept that I'm trying to convey is that separating your presentation from your behavior gives you more flexibility. That can still be beneficial when your components are only ever used once. When your behavior has to change in a way you didn't initially architect your component to support, your presentation layer can be in the best possible situation to be able to handle those changes.

Top comments (0)