React components have a habit of growing over time. Before I know it, some of my app's components will be monstrosities.
But is this actually a problem? After all, it seems a little odd to create many small components that are used only once...
James is the editor of React Armory, and has been creating things with JavaScript for over 15 years.
Read more by JamesFollow @james_k_nelson on TwitterThere is nothing inherently wrong with having large components in a React app. In fact, for stateful components, it is absolutely expected that they’ll have a bit of size.
The thing about state is that it generally doesn’t decompose very well. If there are multiple actions that act on a single piece of state, they’ll all need to be placed in the same component. The more ways that the state can change, the larger the component gets. And if a component has actions that affect multiple types of state, the component will become massive. This is unavoidable.
But even if large components are inevitable, they’re still horrible to work with. And that’s why you’ll want to factor out smaller components where possible, following the principle of separation of concerns.
Of course, this is easier said than done.
Finding the lines that separate concerns is more art than science. But there are a few common patterns you can follow…
In my experience, there are four types of components that you can factor out from larger components.
For more information on view components (which some people call presentational components), read Dan Abramov’s classic, Presentational and Container Components.
View components are the simplest type of component. All they do is display information and emit user input via callbacks. They:
Some signs that you can factor out a presentation component from a larger component include:
Some examples of presentation components that can be factored out of larger components:
onChange
callback).Control components are components that store state related to partial input, i.e. state that keeps track of actions the user has taken, which haven’t yet resulted in a valid value that can be emitted via an onChange
callback. They are similar to presentation components, but:
Some signs that you can factor out a control component from a larger component include:
Some examples of control components include:
You’ll often find that you have a number of controls with the same behavior, but different presentation. In these cases, it makes sense to factor out the presentation into View components, which are passed in via a theme
or view
prop.
You can see a real-world example of connector functions in the react-dnd library.
When factoring presentation components out of controls, you may find that passing individual ref functions and callbacks to the presentation component via props
feels a little wrong. In this case, it may help to pass connector function instead, which clones refs and callbacks onto a passed in element. For example:
class MyControl extends React.Component {
// A connector function uses React.cloneElement to add event handlers
// and refs to an element created by the presentation component.
connectControl = (element) => {
return React.cloneElement(element, {
ref: this.receiveRef,
onClick: this.handleClick,
})
}
render() {
// You can pass a presentation component into your controls via props,
// allowing controls to be themed with arbitrary markup and styles.
return React.createElement(this.props.view, {
connectControl: this.connectControl,
})
}
handleClick = (e) => { /* ... */ }
receiveRef = (node) => { /* ... */ }
// ...
}
// The presentation component can wrap an element in `connectControl` to add
// appropriate callbacks and `ref` functions
function ControlView({ connectControl }) {
return connectControl(
<div className='some-class'>
control content goes here
</div>
)
}
You’ll find that control components can often end up surprisingly large. They have to deal with the DOM, which is a large chunk of state that doesn’t decompose. And this makes factoring out control components especially useful; by limiting your DOM interactions to control components, you can keep any DOM-related mess in a single place.
Once you’ve split out your presentation and control code into separate components, most of the remaining code will be business logic. And if there is one thing that I want you to remember after reading this, it is that business logic doesn’t need to be placed in React components. It often makes sense to implement business logic as plain JavaScript functions and classes. For lack of a better name, I call these controllers.
Ok, so there are only three types of React components. But there are still four types of components, because not every component is a React Component.
And not every car is a Toyota (but at least in Tokyo, most of them are).
Controllers generally follow a similar pattern. They:
Some signs that you can factor out a controller from your component:
Some examples of controllers include:
Some controllers are globals; they exist entirely separately from your React application. Redux stores are a good example of global controllers. But not all controllers need to be global. And not all state needs to go in a single controller or store.
By factoring out controller code for your forms and lists into separate classes, you can instantiate these classes as needed in your container components.
Container components are the glue that connects your controllers to presentation and control components. They are more flexible than the other types of components. But they still tend to follow a few patterns. They:
connect
.While you can sometimes factor out Container components from other Containers, this is pretty rare. Instead, it is best to focus your effort on factoring out controllers, presentation components and control components, with whatever is left becoming your containers.
Some examples of containers include:
App
componentconnect
observer
<Link>
component from react-router (because it uses context and affects the environment)What do you call a component that isn't a View, Control, Controller or Container? You just call it a component! Simple, huh?
Once you’ve found a component to factor out, the question becomes where do I put it? And honestly, the answer depends a lot on personal taste. But there is one rule that I think is important:
If the factored out component is only used in one parent, it goes in the same file as the parent.
This is in the interest of making it as easy as possible to factor out components. Creating files is bothersome and takes you out of flow. And if you try to put every component in a different file, you’ll soon start asking yourself “Do I really need a new component”? So start by putting related components in the same file.
Of course, once you do find a place to re-use that component, you may want to move it to its own file. And that makes figuring out which file to put it in a good problem to have.
By splitting out one monolithic component into a number of controllers, presentation components and control components, you increase the total amount of code that needs to be run. This may slow things down a little bit. But it won’t slow it down very much.
The only time I’ve ever encountered performance issues caused by too many components was when I was rendering 5000 cells in a grid on each frame, each with multiple nested components.
The thing about React performance is that even if your application has perceptible lag, the problem is almost certainly not to do with having too many components.
So use as many components as you’d like.
I’ve mentioned a lot of rules in this spiel. So you may be surprised to hear that I don’t actually like hard and fast rules. They’re usually wrong, at least in some cases. So to be clear:
Just because you can factor something out doesn’t mean that you must factor it out.
Let’s say that your goal is to make your code more comprehensible and easier to maintain. This still leaves the question: what is comprehensible? And what is easy to maintain? The answer often depends on who is asking, and that’s why refactoring is more art than science.
For a concrete example, consider this contrived component:
While it would be perfectly possible to factor out the renderItem
into a separate component, would you actually gain anything by doing so? Probably not. In fact, in a file with a number of different components, using the renderItem
method would probably be easier to follow.
So remember: the four types of components are a pattern that you can use when it feels like they make sense. They’re not hard and fast rules. And if you’re quite unsure about whether something needs to be factored out, just leave it be. Because the world won’t end if some components are fatter than others.