02 — Props, State, and Refs — The Three Ways a Component Holds Data
Props, state, and refs blurred into "just variables the component knows about" until I separated them by job. The split that made them click: props are inputs handed down by the parent, state is the component's own memory that drives re-renders, and refs are an escape hatch for values that must persist across renders without triggering them. [1][2] Each one exists for a different reason, and mixing them up is the source of most beginner confusion.
Props: the inputs a parent hands down
Props (short for properties) are the channel a parent uses to configure a child [1]. They're the function-argument equivalent — when I write <Card title="Hello" />, title is a prop the Card component receives. Three things define props:
- They flow top-down. A parent passes them; the child reads them. Data moves parent → child, never the reverse, unless the parent explicitly passes a callback prop the child invokes.
- They're read-only. A component must never mutate its own props [1]. If a value needs to change, that value is state, not props.
- They trigger renders. New prop values from the parent cause the child to re-render.
That second rule is the one I had to take seriously. "Treat props as immutable" is what keeps the component a pure function of its inputs — same props in, same JSX out. The moment I'm tempted to write to a prop, I've actually discovered I need state, or I need to lift the state up to the parent.
State: the component's own memory
State is the component's private memory — values it owns and can change, where each change schedules a re-render [2]. Where props come from outside, state lives inside the component. In a functional component, state is created with the useState hook (covered in its own notes); for now the conceptual point matters more than the syntax:
- State changes are the engine of re-rendering. Mutating a state variable directly does nothing visible — I have to call the setter React gives me, which queues a new render.
- State is per-instance. Two <Counter /> components on the page each keep their own count.
- Updates may be asynchronous and batched, so I read the latest value through the setter's callback form when the next value depends on the previous.
function Counter() {
// count is state: private to this instance, changing it re-renders
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}The contrast with props is the whole story: props are configuration from the parent, state is memory the component controls. A common rookie move is to duplicate a prop into state ("I'll copy initialName into a state variable and edit that"). That's almost always wrong — it creates two sources of truth that drift apart. If the parent's value should drive the child, read the prop directly; if the child owns the editable value, the prop is just a seed.
Refs: the escape hatch that doesn't re-render
Refs are the third channel, and the one I resisted longest. A ref holds a mutable value that persists across renders but does not trigger a re-render when it changes [3][4]. There are two jobs refs are actually for:
- Reaching into the DOM. To focus an input, measure an element, or integrate with a non-React library, I need the actual DOM node. A ref attached via <input ref={myRef} /> gives me that node imperatively [3].
- Storing a value that shouldn't cause renders. A debounce timer ID, a "has this effect run" flag, a cached value — things I need to remember across renders but where a re-render would be pointless or harmful.
function FocusInput() {
const inputRef = useRef(null);
const handleClick = () => inputRef.current.focus(); // imperative DOM access
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>Focus</button>
</>
);
}The rule I follow for refs: if the value should change the UI when it changes, it's state, not a ref. Refs are deliberately outside React's render loop — that's their power and their danger. Every ref write is a place React isn't watching, so it's a place bugs hide. I reach for a ref only when state genuinely can't do the job (DOM access, or a value that must not cause renders).
How I use this
Before I add any data to a component, I run it through the three-question check:
- Does this value come from the parent? → props. Don't copy it into state.
- Does this value belong to this component, and should the UI change when it does? → state. Use the setter, never mutate directly.
- Does this value need to persist across renders but _not_ re-render when it changes (a DOM node, a timer, a cache)? → ref.
That check resolves almost every "where does this data go" question. The remaining one — state shared between siblings — is "lift it up to the common parent," which is also why I reach for context or a state library once the lifting gets deep.
References
[1] React team, "Passing props to a component," react.dev, 2024. [Online]. Available: https://react.dev/learn/passing-props-to-a-component
[2] React team, "State: a component's memory," react.dev, 2024. [Online]. Available: https://react.dev/learn/state-a-components-memory
[3] React team, "Referencing values with refs," react.dev, 2024. [Online]. Available: https://react.dev/learn/referencing-values-with-refs
[4] React team, "Manipulating the DOM with refs," react.dev, 2024. [Online]. Available: https://react.dev/learn/manipulating-the-dom-with-refs
[5] R. Wieruch, "What is the difference between state and props in React?," Stack Overflow, 2023. [Online]. Available: https://stackoverflow.com/questions/27991366/what-is-the-difference-between-state-and-props-in-react
[6] D. Pavlutin, "The complete guide to useRef() and refs in React," dmitripavlutin.com, 2023. [Online]. Available: https://dmitripavlutin.com/react-useref-guide/
Knowledge check · Question 1 of 5
Which data channel is read-only and flows only from parent to child?
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!