I almost skipped past this. It looked like a small refactor. It ended up being the first time React's "one source of truth" idea actually clicked for me instead of just being a phrase I nodded along to.
Here's the project: a grid of colored pads that toggle on/off when clicked. Simple UI, simple idea.
Where I started: derived state
My first instinct was to create state based on data that lived somewhere else in the component tree — what's usually called derived state. It technically worked. Buttons rendered, colors showed up. But something about it felt fragile, and I couldn't articulate why yet.
Where I ended up: one source of truth
The fix was to move the actual state up into App.jsx, and have everything else just receive data and functions as props. My folder looks like this:
1. App.jsx — holds the single source of truth:
export default function App() {
const [pads, setPads] = React.useState(padsData)
function toggle(idR) {
setPads(prevPads => prevPads.map(item => {
return item.id === idR ? { ...item, on: !item.on } : item
}))
}
const buttonElements = pads.map(pad => (
<Pad
key={pad.id}
color={pad.color}
on={pad.on}
toggleFunc={toggle}
id={pad.id}
/>
))
return (
<div className="pad-container">
{buttonElements}
</div>
)
}
2. pads.js — the raw data the state is initialized from:
export default [
{ id: 1, color: "#F18D8B", on: true },
{ id: 2, color: "#F5C280", on: false },
// ...
]
3. Pad.jsx — a dumb component that just renders and reports back:
export default function Pad({ color, on, toggleFunc, id }) {
return (
<button
style={{ backgroundColor: color }}
className={on ? "on" : undefined}
onClick={() => toggleFunc(id)}
>
</button>
)
}
The part that actually confused me
Not the state move itself — that part I could follow. What genuinely took time to reconcile was passing a function as a prop and tracking two different names for the same value:
- In
Pad.jsx:onClick={() => toggleFunc(id)}— here,idis the real value, the actual id of the button that got clicked. - In
App.jsx:function toggle(idR)— here,idRis just a placeholder name, a parameter waiting to receive whatever value gets passed in.
I kept reading idR like it meant something special, like it was doing extra work. It isn't. It's just a name. The moment I stopped trying to find hidden meaning in idR and just traced the value — click happens → id gets passed → toggle receives it as idR → idR gets compared against each item in state — the whole thing clicked into place.
The full loop, in plain language
-
App.jsxholdspadsstate, seeded frompadsData. - It maps over
padsand renders aPadfor each one, passing downcolor,on,id, and thetogglefunction itself. -
Pad.jsxattachestoggleto its button'sonClick, wrapped in an arrow function so it can pass along which button was clicked. -
togglereceives that id (asidR), and callssetPads, mapping over the previous state and flippingononly for the item whose id matches. - React re-renders with the updated state, and only the clicked button's color state changes.
Why this matters more than it looks like
The bug I would've eventually hit with derived state is the classic one: two sources of truth drifting out of sync. Lifting the state up removes that possibility entirely — there's exactly one place the "real" data lives, and everything else is just a reflection of it.
This is day-something of learning React in public, building toward real projects instead of just finishing tutorials. If you've hit this same wall with props/state, I'd like to hear how you reasoned through it.
Repo's on GitHub if you want to see the full thing — link in my profile.
Top comments (0)