How to avoid prop-drilling in React without using Redux (Part 1)

Michał Urbanek - Frontend Engineer

Michał Urbanek

21 November 2018, 10 min read

thumbnail post

What's inside

  1. Let’s start with an example
  2. Prop-drilling
  3. Reducing props passing without Redux

Using Redux to manage your application state means that you’ll be dealing with a number of consequences - some of them beneficial and some detrimental when it comes to how comfortable the development process is.

Here are some things you might not like that much:

  • Redux adds indirection to your state updates, which - in simple updates - makes it a bit harder to follow them,
  • It often results in moving state that properly belongs to a specific component (or component subtree) into the global store, hindering encapsulation,
  • You have to write more lines of code (though if you use Redux for a good reason and understand why you do it, you may have nothing against that).

The benefits of using Redux, on the other hand, are for example:

  • When testing your app manually, you can view all the dispatched actions and undo some of them to return to a certain moment of your test,
  • In case of a bug, you can attach a list of steps the user has taken to the bug report and replay their session,
  • Building collaborative applications (think Google Docs, for example) is easier,
  • Adding an undo feature is easier,
  • For complex state updates, the indirection Redux introduces may actually help in reasoning about them.

If you care about any of the above benefits and are ready to accept the downsides of Redux, feel free to adopt it. There are also some more reasons why people use it - but I think they’re not very good (even though they seem to be the most popular):

  • Ease of keeping the state between component remounts, or even between page reloads, because it exists in single global store,
  • The opportunity to avoid manual passing of data through many components in order to deliver it to one located deep in the component structure.

If you’re considering to use Redux for just one of the two reasons above, and not because of the previously mentioned benefits, there’s a good chance that you will be frustrated with its complexity. Redux wasn’t designed to solve these problems; you can find solutions that are far simpler.

To achieve the former, you can alternatively use React Context API which also allows keeping the state you want to cache in a place where it won’t be lost on component remount and can be easily persisted in localStorage or a database. It also breaks component encapsulation, but doesn’t come with the indirection Redux introduces.

In this article, I will focus on the second of these (questionable) reasons.

I will describe the phenomenon of passing props through many levels of a component tree more thoroughly, try to evaluate whether that poses a problem, and explain how to limit it (when necessary) without using Redux, instead using Inversion of Control, render props and React Context API.

Let’s start with an example

We often work on applications where some data needs to be accessed from two or more components - sometimes residing far away from each other on the component tree.

Let's have a look at an example with the following component structure (the arrows mark components that need to access the same data):

App Header HeaderTop HeaderBottom SidePanel SearchBar <-- AnimalPage AllAnimalsList <-- AnimalList


You can see the source code (not using Redux) below. In this simple application, the term typed into the search bar is used to filter the list of animals. Both SearchBar and AllAnimalsList components need to access the search term.

Because of that, we need to lift it up and put it in the state of their lowest common ancestor, which in our case is App.

Here’s a live demo: https://codesandbox.io/s/k2vxxy53wv

class App extends Component { constructor () { super() this.state = { searchTerm: '' } }

updateSearchTerm = (term) => { this.setState({searchTerm: term}) }

render () { const {searchTerm} = this.state return ( <div className="App"> <Header searchTerm={searchTerm} handleInputChange={this.updateSearchTerm}/> <AnimalPage searchTerm={searchTerm}/> </div> ) } }

const Header = ({searchTerm, handleInputChange}) => ( <div className="Header"> <HeaderTop/> <HeaderBottom searchTerm={searchTerm} handleInputChange={handleInputChange}/> </div> )

const HeaderTop = () => ( <div className="HeaderTop"> <div className="HeaderTop-logo">?</div> </div> )

const HeaderBottom = ({searchTerm, handleInputChange}) => ( <div className="HeaderBottom"> Welcome to our site! <SidePanel searchTerm={searchTerm} handleInputChange={handleInputChange}/> </div> )

const SidePanel = ({searchTerm, handleInputChange}) => ( <div className="SidePanel"> Search: <SearchBar searchTerm={searchTerm} handleInputChange={handleInputChange}/> </div> )

const SearchBar = ({searchTerm, handleInputChange}) => ( <input value={searchTerm} onChange={event => handleInputChange(event.target.value)}/> )

const AnimalPage = ({searchTerm}) => ( <div className="AnimalPage"> <AllAnimalsList searchTerm={searchTerm}/> </div> )

class AllAnimalsList extends Component { constructor () { super() this.state = { // normally, we would fetch this data from some backend animals: ['Dog', 'Cat', 'Duck', 'Chupacabra', 'Mouse', 'Human', 'Horse', 'Cow', 'Pig', 'Parrot'] } }

render () { const filteredAnimals = this.state.animals.filter( animal => animal.toLowerCase().indexOf(this.props.searchTerm.toLowerCase()) !== -1 ) return <AnimalList animals={filteredAnimals}/> } }

const AnimalList = ({animals}) => ( animals.map(animal => ( <div className="Animal">{animal}</div> )) )

As you can see, to know the current search term and be able to modify it the SearchBar component accepts two props: searchTerm and handleInputChange. The values for these props need to be passed from App all the way down through Header, HeaderBottom and SidePanel components that don't use the props themselves. Their only concern with these two props is passing them further.

Prop-drilling

This phenomenon of passing props through components that don't directly use them was dubbed as "prop-drilling" by React community. Most people agree that it feels redundant and can be annoying to maintain. For example, if SearchBar needs one more prop, we have to add it to each of the intermediate components. If we change our mind and decide that SearchBar should now be rendered in HeaderTop instead, we would have to remove the two props from HeaderBottom and SidePanel, and add them to HeaderTop. Basically, wherever SearchBar moves, the props follow.

In our case, it's not that much work but it can quickly become one with more props and more component nesting levels. In my experience, the desire to avoid prop-drilling is the most popular reason why developers reach for Redux.

But consider this:

Most of the time we actually want prop-drilling.

Prop-drilling makes it clear where data comes from (by checking where searchTerm comes from) and who can change it (by checking where updateSearchTerm is passed to). Why is that important? Let's say searchTerm has an unexpected value. You need to check only the places to which updateSearchTerm is passed in order to find the code that sets it to an incorrect value.

It's only sometimes that the number of props passed through many tree levels gets overwhelming. Our example is not one of these cases - it's simple enough to stay exactly in its current form.

But let’s see how to reduce the number of passed props in case you experience a serious prop-drilling-related complexity in your project - and how to do it without Redux.

Reducing props passing without Redux

We could reduce the number of passed props by making use of the Inversion of Control software design principle.

Basically, we want to instantiate SeachBar higher in the component tree and pass this instance as a prop, instead of searchTerm and handleInputChange props. As the first step, let's change HeaderBottom to instantiate SearchBar and pass it to SidePanel as a searchBar prop, which SidePanel just renders:

const HeaderBottom = ({searchTerm, handleInputChange}) => ( <div className="HeaderBottom"> Welcome to our site! <SidePanel searchBar={<SearchBar searchTerm={searchTerm} handleInputChange={handleInputChange}/>} /> </div> )

const SidePanel = ({searchBar}) => ( <div className="SidePanel"> Search: {searchBar} </div> )

We have just reduced the number of props passed to SidePanel to one.

Let's proceed to the second step and change HeaderBottom to be more generic, rendering anything it gets as a children prop. At the same time, let's move SearchBar to be instantiated even higher - in Header - and pass it, wrapped in SidePanel, to the new version of HeaderBottom:

const Header = ({searchTerm, handleInputChange}) => ( <div className="Header"> <HeaderTop/> <HeaderBottom> <SidePanel> <SearchBar searchTerm={searchTerm} handleInputChange={handleInputChange}/> </SidePanel> </HeaderBottom> </div> )

const HeaderBottom = ({children}) => ( <div className="HeaderBottom"> Welcome to our site! {children} </div> )

Now we have limited the number of props passed to HeaderBottom component. We have also changed it to be more flexible, rendering whatever we pass to it as children (you can read more about this approach in React docs).

Let's do the last step of our refactoring. We’re now going to instantiate SearchBar in App and pass it to Header as a searchBar prop.

class App extends Component { //...

render () { const {searchTerm} = this.state return ( <div className="App"> <Header searchBar={<SearchBar searchTerm={searchTerm} handleInputChange={this.handleInputChange}/>} /> <AnimalPage searchTerm={searchTerm}/> </div> ) } }

const Header = ({searchBar}) => ( <div className="Header"> <HeaderTop/> <HeaderBottom> <SidePanel> {searchBar} </SidePanel> </HeaderBottom> </div> )

Note that we could have done it like this as well:

class App extends Component { //...

render () { const {searchTerm} = this.state return ( <div className="App"> <Header headerBottom={ <HeaderBottom> <SidePanel> <SearchBar searchTerm={searchTerm} handleInputChange={this.handleInputChange}/> </SidePanel> </HeaderBottom> } /> <AnimalPage searchTerm={searchTerm}/> </div> ) } }

const Header = ({headerBottom}) => ( <div className="Header"> <HeaderTop/> {headerBottom} </div> )

That would be analogous to what we did in the second step with HeaderBottom, sending an entire subtree for it to render as a children prop. Here, we use headerBottom prop instead which feels more correct in this case as HeaderTop is also a child.

Both versions we listed above may make sense, but in our case we will go with the first one. We don't want to give away control over what Header renders after HeaderTop. Instead, we always want Header to render HeaderBottom with SidePanel inside, and only let the parents decide what they will pass as searchBar.

The resulting code is visible below.

Check out the live demo: https://codesandbox.io/s/2xy1mv3rmp

class App extends Component { constructor () { super() this.state = { searchTerm: '' } }

updateSearchTerm = (term) => { this.setState({searchTerm: term}) }

render () { const {searchTerm} = this.state return ( <div className="App"> <Header searchBar={<SearchBar searchTerm={searchTerm} handleInputChange={this.updateSearchTerm}/>}/> <Page> <AllAnimalsList searchTerm={searchTerm}/> </Page> </div> ) } }

const Header = ({searchBar}) => ( <div className="Header"> <HeaderTop/> <HeaderBottom> <SidePanel> {searchBar} </SidePanel> </HeaderBottom> </div> )

const HeaderTop = () => ( <div className="HeaderTop"> <div className="HeaderTop-logo">?</div> </div> )

const HeaderBottom = ({children}) => ( <div className="HeaderBottom"> Welcome to our site! {children} </div> )

const SidePanel = ({searchBar}) => ( <div className="SidePanel"> Search: {searchBar} </div> )

const SearchBar = ({searchTerm, handleInputChange}) => ( <input value={searchTerm} onChange={event => handleInputChange(event.target.value)}/> )

const Page = ({children}) => ( <div className="Page"> {children} </div> )

class AllAnimalsList extends Component { constructor () { super() this.state = { animals: ['Dog', 'Cat', 'Duck', 'Chupacabra', 'Mouse', 'Human', 'Horse', 'Cow', 'Pig', 'Parrot'] } }

render () { const filteredAnimals = this.state.animals.filter( animal => animal.toLowerCase().indexOf(this.props.searchTerm.toLowerCase()) !== -1 ) return <AnimalList animals={filteredAnimals}/> } }

const AnimalList = ({animals}) => ( animals.map(animal => ( <div className="Animal">{animal}</div> )) )


After our change, only one prop is passed through each of Header, HeaderBottom and SidePanel components. If we needed to send two additional props to SearchBar, we would pass them directly to its instance in App. If we changed our mind and wanted to render SearchBar in HeaderTop instead, we would need to delete only one prop from SidePanel and add as little as one prop (e.g. searchBar) to HeaderBottom. Hurray!

I will wrap up the first part of this blog post series here.

In the second part, we will see how to reduce prop drilling in more complex situations using render props and React Context API.

Michał Urbanek - Frontend Engineer

Michał Urbanek

Frontend Engineer

Michał holds an MSc degree in Computer Science from AGH UST in Cracow. After a few years of working as a fullstack developer for a fintech company, Michał decided to focus on frontend. He launched a successful personal project, a Flappy Bird game clone generator which peaked at 20k daily users. Michał likes to work on projects that have a positive impact and help solve pressing society and environment-related problems. In his free time, he dances West Coast Swing, plays squash, and practices digital nomadism.

Tags

javascript
react
redux

Share

Recent posts

See all blog posts

Are you ready for your next project?

Whether you need a full product, consulting, tech investment or an extended team, our experts will help you find the best solutions.

Hi there, we use cookies to provide you with an amazing experience on our site. If you continue without changing the settings, we’ll assume that you’re happy to receive all cookies on Sunscrapers website. You can change your cookie settings at any time.