A fews days ago i found a reddit thread where people posted about "bad habits of react developers".
"Prop drilling" was mentioned a few times, which reminded me of a problem I just recently came across at work: overusing useSelector
to avoid prop drilling.
For some reason react devs are really scared of prop-drilling. They reach for useSelector
too often and too carelessly, without thinking of the negative impact on their codebase.
Prop-drilling is a practice where you pass a data from one component to another, further down the component-tree.
I've set up a tiny react-typescript + redux example in codesandbox, that allows you to log in and visit a UserPage
when logged in.
The code in App.tsx
and AppNoProps.tsx
is functionally identical. AppNoProps.tsx
is using useSelector
over prop drilling and I'll walk you through why I prefer the implementation of App.tsx
.
you might have to open the codesandbox in a full window, to get typescript working. for some reason it shows
any
instead of the real types when embedded 🙄
Lets dive into AppNoProps.tsx
.
If we hover over user
in const user = useSelector(...)
in either App
or UserPage
we see that it is typed User | null
. This is the case whenever we use useSelector
.
But in <App>
we only render <AuthenticatedApp>
and its child <UserPage>
if user
is NOT null
(truthy).
Even though <UserPage>
is only rendered if user
is not null
, we have to do null-checks all over again in <UserPage>
. The fact that we already checked user
for null
in <App>
does not help us at all.
Whenever we want to access the value of user
we have to check for null
again. That means adding if
conditionals in every hook, and the return
of our component.
Please dont use a type assertion like
const user = useSelector(...) as User
as this just overrides/disables type checking.
In our small example, a few if
statements might not look like much of a problem, but dont forget that we have to repeat these checks for any children of <UserPage>
too. All the redundant if
statements and optional chaining increase our cyclomatic complexity and make the flow of our code hard to follow.
Read about Cyclomatic Complexity.
so whats the alternative?
In <App>
we already know for sure that user is either null
or User
, so by checking whether user
is null, we can narrow its type further down the component tree. All we need to change is pass user
down the subtree of <AuthenticatedApp>
.
Hover over the user
value in <AuthenticatedApp user={user} />
and notice how it is typed as User
. From here on out we can lean back and trust that we have user
available.
Here's where the only drawback comes into play. Our <AuthenticatedApp>
needs to accept an additional user
prop, even though it doesnt do anything with it except passing it on to <UserProp>
.
Lets check out <UserPage>
and see what we've gained:
No more null checks! Our useEffect
can run without any if
statements and we dont need an early return if user
is null either. We have basically eliminated any need for null checks or optional chaining for the whole component tree below AuthenticatedApp
.
tldr: Use useSelector
to select state from the store at the highest point you need it, narrow its type to remove null
as possible value and pass it down as prop. Its basically "lifting state up" but for redux.
Depending on the distance between where you first need the user
and the last leaf node where you also need the user, you might not prop-drill all the way. Lets say our App were more deeply nested like App > AuthenticatedApp > Layout > SubLayout > UserPage > UserSettingsPage > UserForm
. In this case I might not prop-drill the user
value from <App>
all the way down to UserForm. Instead I would decide whats the closest ancestor of <UserForm>
that needs user
and user a combination of prop-drilling and composition from there. Here the closest ancestor would probably be <UserPage>
. That way I dont need to drill through <Layout>
and <SubLayout>
, but I still reduce the number of null-checks in the <UserPage>
tree.
In development any "best practice" or "antipattern" come with tradeoffs, so it is on us to weigh the pros and cons and decide what works better for us.
That said, I would pick simple, readable code over loads of if
s and optional chaining any time, even if it means adding a few more props to components.