loading...
Cover image for Component vs Prop drilling in React

Component vs Prop drilling in React

patroza profile image Patrick Roza Originally published at patrickroza.com on ・3 min read

Recently, often the question is asked if Hooks and Context replaces Redux. It is more a question of do you need Redux, and perhaps how Hooks and Context can help you write cleaner, more functional, more composable code.

I stumbled upon these topics in:

In these series, I will try to make a point why I think you can achieve most usecases without needing Redux, and perhaps how Hooks and Context may help you with that.

Let's first examine what is often recommended as alternative to Redux:

  1. A container component that centralizes the state, and then leverages "prop drilling" to pass props down to all child components. Creating coupling between the components, sometimes multiple levels deep.
  2. Multiple components that manage their own state and upgrade to Context/Provider API once state is more shared. Continueing "prop drilling". Also as clients sometimes have a hard time to make up their mind, it may lead to a lot of jumping around of where the state should live.

Improvement 1: Component Drilling instead of Prop Drilling

A prop drilling sample:

const SomeScreen = ({ someAction, someProp, someOtherProp, someOtherChildProp }) => (
  <div>
    <SomeComponent someProp={someProp} />
    <SomeOtherComponent
      someOtherProp={someOtherProp}
      someOtherChildProp={someOtherChildProp}
      action={someAction}
    />
  </div>
)

const SomeOtherComponent = ({action, someOtherProp, someOtherChildProp}) => (
  <div>
    <SomeOtherChildComponent
      prop={someOtherChildProp}
      action={action}
    />
  </div>
)

What makes this prop drilling is the fact that SomeOtherComponent takes someOtherChildProp and someAction, which are actually props of the SomeOtherChildComponent.

Now with component drilling:

const SomeScreen = ({ someAction, someProp, someOtherProp, someOtherChildProp }) => (
  <div>
    <SomeComponent someProp={someProp} />
    <SomeOtherComponent someOtherProp={someOtherProp}>
      <SomeOtherChildComponent
        someProp={someOtherChildProp}
        action={someAction}
      />
    </SomeOtherComponent>
  </div>
)

Here we stopped making SomeOtherComponent responsible to pass the props for SomeOtherChildComponent. Of course this moves the coupling from SomeOtherComponent to the Screen instead. Better; it lives closer to the definition, there are less players involved in coupling.

Improvement 2: State in Hooks/HOCs, so that it can be easily upgraded/downgraded from shared to local state etc.

The goal is to decouple the 'detail' if state is local, shared, or global. Also, the source of the state will be abstracted (imagine that some of the state comes from REST/GraphQL/localStorage it doesn't matter)

// written as hooks, but can be written as HOCs as well
const useSomeProp = () => {
  const someProp = // ...
  return { someProp }
}
const useSomeOtherProp = () => {
  const someAction = // ...
  const someOtherProp = // ...
  const someOtherChildProp = // ...
  return { someAction, someOtherProp, someOtherChildProp }
}

const SomeScreen = () => {
  const { someProp } = useSomeProp()
  const { someAction, someOtherProp, someOtherChildProp } = useSomeChildProp()
  return (
    <div>
      <SomeComponent someProp={someProp} />
      <SomeOtherComponent someOtherProp={someOtherProp}>
        <SomeOtherChildComponent
          someProp={someOtherChildProp}
          action={someAction}
        />
      </SomeOtherComponent>
    </div>
  )
}

As you can see, the props now come from 2 hooks (which could be written as HOCs as well)

Now imagine that we want to use the same state of useSomeOtherProp elsewherec:

const SomeContext = createContext()
const useSomeOtherProp = () => {
  const someAction = // ...
  const { someOtherProp, someOtherChildProp } = useContext(SomeContext)
  return { someAction, someOtherProp, someOtherChildProp }
}

// Wrap the `<SomeContext.Provider value={...state} />` around the `SomeScreen`

Now imagine that SomeOtherComponent has to become more nested, or used in other places:

const SomeOtherComponent = () => {
  // moved from SomeComponent
  const { someAction, someOtherProp, someOtherChildProp } = useSomeChildProp()
  return (
    <div>
      <h1>{someOtherProp}</h1>
      <SomeOtherChildComponent
        someProp={someOtherChildProp}
        action={someAction}
      />
    </div>
  )
}

// Move the <SomeContext.Provider where-ever it makes sense to be able to access the shared state.

Slots

Of course component drilling does not mean only children, you can also drill components through props ;-)

const SomeOtherComponent = ({ children, left, right }) => (
  <div>
    <div>{left}</div>
    <div>{children}</div>
    <div>{right}</div>
  </div>
)

// Usage
<SomeOtherComponent
  left={<SomeChildComponent1 action={someAction} />}
  right={<SomeChildComponent2 ... /> }
>
   Some center content
</SomeOtherComponent>

In closing

There's of course more to it, more trade-offs to consider, room for optimizations etc. But we can address them once they become relevant. That's the beauty of a flexible architecture; make things just flexible/plugable enough so that once changed requirements come (and they will), you can respond to them quickly.

Discussion

markdown guide
 
 

thanks your the article. in summary, "keep your views shallow and do all the routing in one place"