One disadvantage of the top-down rendering approach we use is that the closer to the root of the application a component lives, the more it will be rerendered. The root component will be rerendered every time any data changes.
This is probably not a big deal for applications without much dynamic state, but let's say you have a input box at the bottom of a large tree of components (which will happen on pretty much any page containing a form). The user types, and every keystroke causes the entire app to rerender!
Well, this might be a big deal, or it might not be, depending on how you design your components. Some general guidelines will help us architect React apps that are fast by default, and don't need a whole lot of manual optimization.
When you're looking at a component's render()
method, you can tell how expensive it will be by counting how many DOM elements it contains. That's because DOM elements are always recreated as virtual DOM nodes. Components, on the other hand, are just placeholders -- they might trigger further work in their own render()
methods, or they might not. Components are memoized -- that is, they can run a computation once and return the same result every time subsequent time they're called.
As a result, it makes sense to split up applications along boundaries of "things that will change" and "things that won't". Here's an example of what we don't want to do:
const App = component(({ state }) => {
return (
<div className="header">
<h1>Find Something</h1>
</div>
<div className="row">
<SearchBar query={state.cursor('query')} />
</div>
<div className="row">
<ResultsList results={state.cursor('results')} />
</div>
<div className="footer">
<p>Copyright uShip</p>
</div>
);
});
There's a lot of DOM elements (AKA a lot of work) going on at the top level, making rerenders expensive. How can we fix this?
// All static HTML
const Header = component(() => {
return (
<div className="header">
<h1>Find Something</h1>
</div>
);
});
const Footer = component(() => {
return (
<div className="footer">
<p>Copyright uShip</p>
</div>
);
});
const App = component(({ state }) => {
return (
<Header />
<div className="row">
<SearchBar query={state.cursor('query')} />
</div>
<div className="row">
<ResultsList results={state.cursor('results')} />
</div>
<Footer />
);
});
Better -- we're not rendering nearly as many DOM elements on each pass. One further optimization is to recognize presentational or layout elements:
// All static HTML
const Header = component(() => {
return (
<div className="header">
<h1>Find Something</h1>
</div>
);
});
const Footer = component(() => {
return (
<div className="footer">
<p>Copyright uShip</p>
</div>
);
});
const Row = component(({ children }) => {
return (
<div className="row">
{children}
</div>
);
});
const App = component(({ state }) => {
return (
<Header />
<Row>
<SearchBar query={state.cursor('query')} />
</Row>
<Row>
<ResultsList results={state.cursor('results')} />
</Row>
<Footer />
);
});
There's an additional benefit here: Our top-level app's organization is even more clear, since components can declaratively describe their role.
This is what I mean by pushing work down the tree. The high-level components -- the ones that depend on a lot of different parts of the application's state -- should be responsible for marshalling data and governing the relationship of subcomponents, and not much (if any) actual rendering. Components that handle rendering will ideally do so for only one discrete piece of state: Single Responsibility Principle for components!
The below gist is an example of how we could implement general-purpose presentation components to help make this approach practical.