š¦ If youāre unfamiliar with the concept of āProp Drillingā then please read this blog post before going forward.
Letās skip to the end here. Itās surprising what you can accomplish by passing react elements rather than treating components as uncrossable boundaries. Weāll have a practical example in our exercise, so let me show you a quick and easy contrived example to explain what weāll be doing here:
function App() {const [count, setCount] = useState(0)const increment = () => setCount(c => c + 1)return <Child count={count} increment={increment} />}function Child({ count, increment }: { count: number; increment: () => void }) {return (<div><strong>I am a child and I don't actually use count or increment. My child doesthough so I have to accept those as props and forward them along.</strong><GrandChild count={count} onIncrementClick={increment} /></div>)}function GrandChild({count,onIncrementClick,}: {count: numberonIncrementClick: () => void}) {return (<div><small>I am a grand child and I just pass things off to a button</small><button onClick={onIncrementClick}>{count}</button></div>)}
This prop drilling stuff is one of the reasons so many people have jumped onto state management solutions, whether it be libraries or React context. However, if we restructure things a bit, weāll notice that things get quite a bit easier without losing the flexibility weāre hoping for.
function App() {const [count, setCount] = useState(0)const increment = () => setCount(c => c + 1)return (<ChildgrandChild={<GrandChildbutton={<button onClick={onIncrementClick}>{count}</button>}/>}/>)}function Child({ grandChild }: { grandChild: React.ReactNode }) {return (<div><strong>I am a child and I don't actually use count or increment. My child doesthough so I have to accept those as props and forward them along.</strong>{grandChild}</div>)}function GrandChild({ button }: { button: React.ReactNode }) {return (<div><small>I am a grand child and I just pass things off to a button</small>{button}</div>)}
Now, clearly you can take this too far (our contrived example above probably does), but the point is that by structuring things a little differently, you can keep the components that donāt care about state free of the plumbing needed to make it work. If we decided we needed to manage some more state in the App and that was needed in the button then we could update only the app for that.
This style of composition has helped me reduce the amount of components and
files I touch (break?) when I need to make a change and itās also made my
abstractions much easier (imagine if we wanted to reuse the Child
from above
but needed to customize the grandChild
. Much easier when weāre just accepting
a prop for it).
When we structure our components to only really deal with props it actually cares about, then it becomes more of a ālayoutā component. A component responsible for laying out the react elements it accepts as props. If youāre familiar with Vue, this concept is similar to the concept of scoped slots.
š Read more about this in my blog post: One React mistake thatās slowing you down
Real World Projects that use this pattern:
šØāš¼ In this exercise weāve got a simple user interface with several components necessitating passing state and updaters around. Weāre going to restructure things so we pass react elements instead of state and updaters. We might be going a tiny bit overboard, but the goal is for this to be instructive for you.
By the time youāre done, the whole UI should look and function exactly as before, but youāll get a sense for how to use this pattern. The tests will be there just for you to verify you havenāt broken anything that should be working if you want to use them.
import { useState } from 'react'import * as ReactDOM from 'react-dom/client'import { SportDataView, allSports } from '#shared/sports.tsx'import { type SportData, type User } from '#shared/types.tsx'function App() {const [user] = useState<User>({ name: 'Kody', image: '/img/kody.png' })const [sportList] = useState<Array<SportData>>(() => Object.values(allSports))const [selectedSport, setSelectedSport] = useState<SportData | null>(null)return (<divid="app-root"style={{ ['--accent-color' as any]: selectedSport?.color ?? 'black' }}><Nav avatar={<img src={user.image} alt={`${user.name} profile`} />} /><div className="spacer" data-size="lg" /><Mainsidebar={<ListlistItems={sportList.map(p => (<li key={p.id}><SportListItemButtonsport={p}onClick={() => setSelectedSport(p)}/></li>))}/>}content={<Details selectedSport={selectedSport} />}/><div className="spacer" data-size="lg" /><FooterfooterMessage={`Don't have a good dayāhave a great day, ${user.name}`}/></div>)}function Nav({ avatar }: { avatar: React.ReactNode }) {return (<nav><ul><li><a href="#/home">Home</a></li><li><a href="#/about">About</a></li><li><a href="#/contact">Contact</a></li></ul><a href="#/me" title="User Settings">{avatar}</a></nav>)}function Main({sidebar,content,}: {sidebar: React.ReactNodecontent: React.ReactNode}) {return (<main>{sidebar}{content}</main>)}function List({ listItems }: { listItems: Array<React.ReactNode> }) {return (<div className="sport-list"><ul>{listItems}</ul></div>)}function SportListItemButton({sport,onClick,}: {sport: SportDataonClick: () => void}) {return (<buttonclassName="sport-item"onClick={onClick}style={{ ['--accent-color' as any]: sport.color }}aria-label={sport.name}><img src={sport.image} alt={sport.name} /><div className="sport-list-info"><strong>{sport.name}</strong></div></button>)}function Details({ selectedSport }: { selectedSport: SportData | null }) {return (<div className="sport-details">{selectedSport ? (<SportDataView sport={selectedSport} />) : (<div>Select a Sport</div>)}</div>)}function Footer({ footerMessage }: { footerMessage: string }) {return (<footer><p>{footerMessage}</p></footer>)}const rootEl = document.createElement('div')document.body.append(rootEl)ReactDOM.createRoot(rootEl).render(<App />)
Quick Links
Legal Stuff
Social Media