Redux can eliminate the need for component state by storing all data in one immutable object. However Redux introduces new complexity to manage global state and actions. For domain resources (usually fetched over HTTP) a global store enables sophisticated caching strategies. But for application state, it’s often more trouble than it’s worth.
React lets us organize applications by the features in our domain (e.g. ProjectPage, Search, ValidatedField) rather than by technical patterns (e.g. views, containers, actions, reducers). Effective product teams work in terms of features, not abstract technical domains.
Deciding to work with component state puts pressure on our designs. Developers must decide which components should own state and then connect parent and child components to that state with data props and callbacks. Because components can make API calls, manage internal state, and render HTML they often grow with every new feature.
Let’s focus on one design pressure we often feel when using component state: tunneling data though layers of components.
Tunneling Application State
Most front-end developers know that they could forego global state if they were willing to pass every bit of state through every component. This practice is sometimes called “tunneling”.
Tunneling properties through components starts small. But every time a developer extracts an intermediate component, they must tunnel all the properties through that new component. It’s like they’re being punished every time they improve the design.
Let’s look at an example of a search component that follows a naive design path.
Already there’s a little boilerplate in our
render method. We’re mapping
this.handleFilterClear) without providing
any new information. It’s redundant but it’s a reasonable way to expose a
public API to
Now let’s check out the
SearchLayout has some presentation responsibilities (notice the heading and the
horizontal rule), but it also has to divvy up props from its owner to its
children. This time we’re mapping props that our component doesn’t care about
to other props that our component doesn’t care about. This is where we start
to feel the “tunneling” pain.
Search, meet SearchField
SearchLayout shouldn’t know how to connect the prop names between
SearchField. It should only care about presentation things: headings,
horizontal rules, and other stylistic elements.
So let’s pull the configuration of
SearchResult up the stack,
right where that information lives: inside
So what does
SearchLayout look like now?
SearchLayout decides where to put the
field prop and
results prop, but
no longer needs to worry about the props exchanged between
Is this actually better?
Are we really improving our design or just moving stuff around?
I argue this design is better because we’ve eliminated the repetition of
tunneling our properties through the
SearchLayout. We now map the
properties to owned components once rather than twice. We’ve also identified
an implicit responsibility (configuration) that was shared between two
components and is now confined to
However, if we look at the
Search component before and after this
refactoring we notice that it’s more complicated. Although
now dead simple,
Search knows about three components (
SearchResults) rather than one (
Search is the top-level component in our
Search domain, it’s easy for
responsibilities to collect here.
Search handles both the concern of querying and the concern of
coordinating communication between our search components. Let’s extract a
new component to handle our lower-level, “querying” concern.
Introducing the SearchQuery Component
An alternative, and probably a more popular option, is to create a higher
order component that accepts
Search as an argument and renders it with
query prop. I prefer the render callback because it declaratively shows
how all of our search components work together inside of
Notice that I cheated a little. Not only did I extract a component to handle
querying, I also bundling the props and callbacks into a domain object that
query inside of
Search is looking pretty good at this point.
We set out to remove some
prop tunneling and at the same time dramatically
increased the flexibility of our design. It would be trivial to create
Search component that uses a different
SearchQuery component to
accommodate a different backend. We could inject a different presentation for
our search results into our
SearchLayout component. The flexibility comes
with a cost: separating concerns creates more abstract pieces to learn about.
But in this case, the level of abstraction in each component is more
consistent, making our components easier to understand.
If you squint a little you can see
query as a little Redux bundle with one
immutable state object and 3 actions (setFilter, clearFilter, setTerm).
By creating a domain object and a component dedicated to composition, we removed the state tunneling pain. As a happy side-effect we created a design that is more flexible, has constant levels of abstraction, and is easier to test.