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>);
}
What are the features of this toggle?
- You can set an initial states
- It maintains its own state
- 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.
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.
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?
- Validation based on external values, e.g., is this value already present in a list of values you've entered prior?
- 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>);
}
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;
}
Hooks
const useToggle = (initialState = false) => {
const [isOn, setIsOn] = useState(initialState);
return [isOn, () => {
/
const nextValue = toggle(isOn);
setIsOn(nextValue);
return nextValue
}];
};
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);
}
}/>);
};
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)