Imagine that we have a very simple React
component Hello
which greets a user with a message:
import React from 'react'; import PropTypes from 'prop-types'; export default function Hello({ name }) { return (
Hello, {name}!
); } Hello.propTypes = { name: PropTypes.string.isRequired };
And here is how we use it:
Great simple component, we really like the code. But it turns out that the name
value can be null
until we fetch it from a Back-End. We don’t want our users to see a stub: Hello, !. So we decided to hide the text if the name
is null
:
import React from 'react'; import PropTypes from 'prop-types'; export default function Hello({ name }) { if (name == null) { return null; } return (
Hello, {name}!
); } Hello.propTypes = { name: PropTypes.string.isRequired };
Looks good! Stub has gone and the message shows up only when the profile is loaded. But there is another issue – some noisy error message in the console:
Warning: Failed prop type: Required prop 'name' was not specified in 'Hello'
Ouch, because name
is null
it doesn’t pass propTypes
validation. Maybe we need to provide some default value like empty string and check for the name not to be empty?
This escalates quickly and we are now shedding crutches instead of focusing on the business logic of the component. What if we have dozens of required properties – name, age and other data which will come later. It doesn’t sound cool if we have to set up some default values for all of them.
Of course, this case can be handled directly by JavaScript outside of the component:
This solution is okay but what if we use Redux
and provide name
value directly to container:
import { connect } from 'react-redux'; import Hello from '../../components/Hello'; const mapStateToProps = (state) => { const user = state.user; return { name: user.name }; }; export default connect(mapStateToProps)(Hello);
Now we need to pass the user
object to the Hello’s parent for using JavaScript-hiding. Or we should rollback to the uglier solution and handle empty state within Hello
.
Solution
But can we do better? Can we have a simple, original, neat version of Hello
with a normal propTypes
validation and make them hideable on some condition? As a bonus, we want to extract such behavior and reuse with other components.
It’s actually doable! Because React components are composable, we can easily create Higher Order Component(HOC) which accepts original always-shown component and turns it into hideable when some condition is met. Here is the code of such HOC
:
import React from 'react'; import PropTypes from 'prop-types'; const propTypes = { hideComponent: PropTypes.bool, children: PropTypes.node }; const defaultProps = { hideComponent: false }; function getDisplayName(component) { return component.displayName || component.name || 'Component'; } export default function Hideable(component) { function HideableComponent({ hideComponent, children, ...props }) { if (hideComponent) { return null; } return React.createElement(component, props, children); } HideableComponent.displayName = `Hideable(${getDisplayName(component)})`; HideableComponent.propTypes = propTypes; HideableComponent.defaultProps = defaultProps; return HideableComponent; }
So Hideable
is a simple function which wraps any passed-in component into a proxy HideableComponent
. This proxy component has only one real property – hideComponent
and if the value of hideComponent
is truthy – child component will not be rendered:
Let’s make our Hello
hideable and wrap it into Redux
:
import { connect } from 'react-redux'; import Hello from '../../components/Hello'; import Hideable from '../../utils/hideable'; const HideableHello = Hideable(Hello); const mapStateToProps = (state) => { const user = state.user; if (!user.name) { return { hideComponent: true }; } return { name: user.name }; }; export default connect(mapStateToProps)(HideableHello);
Now mapStateToProps
manages visibility of the Hello
component and we haven’t changed anything within the original Hello
component.
Conclusion
So we started from a very simple component and ended up with the same component topped-up with hideable logic. Using the same HOC approach, you can build more complex shareable behaviors and use them to build complex components from the simple ones.
That’s it! I hope this neat trick will help you write simpler React components and reduce an amount of a boilerplate code.