In Defense of Non-Presentational React Components
By: The LaunchPad Lab Team / June 1, 2017
React components, especially stateless functional components, are typically used to generate UI elements on a web page. However, a React component doesn’t necessarily need to render anything at all. A classic example of this behavior is the Route component from react-router: it’s a bona fide React component, yet it doesn’t add anything new to the DOM.
So if it doesn’t contribute to the view, what’s the purpose of a non-presentational component like Route? Answer: it offers a clean alternative for passing props to a parent component.
Let’s illustrate this concept with an example where directly passing props to a component results in some ugly code.
Let’s say we want to build a Table component where each row of the table corresponds to a Javascript object and each column corresponds to a key on that object. Additionally, let’s provide the option to highlight a column or give it a custom label:
function Table ({ rows, columns }) { return ( <table> <thead><tr> { columns.map((column, key) => <Header key={ key } column={ column } /> ) } </tr></thead> <tbody> { rows.map((row, key) => <Row key={ key } row={ row } columns={ columns } /> ) } </tbody> </table> ) } // Highlighting and labeling will be handled // within the Header and Row components. Table.propTypes = { rows: PropTypes.arrayOf(PropTypes.object), columns: PropTypes.arrayOf(PropTypes.shape({ name: PropTypes.string.isRequired, label: PropTypes.string, highlighted: PropTypes.bool, }), }
The API for this component would look something like this:
function PeopleTable ({ people }) { return ( <Table rows={ people } columns={[ { name: 'name', highlighted: true }, { name: 'age' }, { name: 'hair', label: 'Hair Color' }, { name: 'sport', label: 'Favorite Sport' }, ]} /> ) }
Although this style of passing props is ok, having a bunch of objects interpolated into the JSX obscures the presentational aspect of the code. It’s easy to mistake the column definitions as the actual data of the table, rather than just information about the view!
We can solve this problem by creating a non-presentational Column component, which simply receives column information and passes it to its parent. This leaves us with the following syntax:
function PeopleTable ({ people }) { return ( <Table rows={ people }> <Column name="name" highlighted /> <Column name="age" /> <Column name="hair" label="Hair Color" /> <Column name="sport" label="Favorite Sport" /> </Table> ) }
Much better! Now, let’s examine how Table receives information from Column.
First of all, the view logic of Column component should be very simple, seeing as it renders nothing. However, it can still run PropTypes validation on the data it receives, which is very useful:
function Column () { return null } Column.propTypes = { name: PropTypes.string.isRequired, label: PropTypes.string, highlighted: PropTypes.bool, }
Next, the Table component needs to pull column information from its children rather than from its props. We can do that by adding the following lines to its render function:
function Table ({ rows, children }) { const childrenArray = Array.isArray(children) ? children : [children] const columns = childrenArray.map(child => child.props) return ( ... ) } Table.propTypes = { rows: PropTypes.arrayOf(PropTypes.object), children: PropTypes.node.isRequired, }
As you can see, the array of column information is now pulled from the props of the children rather than from Table‘s own props. This makes the render function a little more complicated, but it really pays off in the simplification of the component’s usage.
And that’s all there is to it! Though non-presentational React components are rare, they can be a great tool to have in your arsenal for when props start getting out of hand.
Ready to Build Something Great?
Partner with us to develop technology to grow your business.