Props + State: The Secret Sauce Behind Interactive React

Introduction
If you’re learning React, mastering state and props is essential. These two concepts power React’s data flow and make components interactive — props let components receive configurable data from their parents, while state lets a component manage and update its own data over time.
In this post we’ll cover:
What props are and how to pass data between components
What state is, how to use
useState, and when to choose state over propsHow state and props work together to build responsive, interactive UIs
By the end, you’ll understand when to use props vs state and be able to build React components that manage data and respond to user actions.
What Are Props?
Props (short for “properties”) are read‑only pieces of data passed from a parent component to a child component. They allow components to be reusable and configurable.
Passing Props
function Greeting(props) {
return <h1>Hello, {props.name}!</h1>;
}
function App() {
return <Greeting name="Alice" />;
}
Here the Greeting component receives a name prop and displays it. Props can be any JavaScript value – strings, numbers, arrays, objects, even functions.
Destructuring Props
A cleaner way to access props is to destructure them in the function parameter:
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
Props Are Immutable
A child component must never modify its props. Props are read‑only. If you need to change data, that’s where state comes in.
What Is State?
State is data that a component manages internally. Unlike props, state can be changed over time (e.g., in response to user input, network responses, etc.).
In functional components, we use the useState hook to add state.
Using useState
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // initial value 0
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useStatereturns an array with two items: the current state value and a function to update it.When
setCountis called, React re‑renders the component with the new state.
Rules of Hooks
Only call hooks at the top level of a functional component (not inside loops, conditions, or nested functions).
Only call hooks from React functions (components or custom hooks).
State vs Props – A Side‑by‑Side Comparison
| Feature | Props | State |
|---|---|---|
| Mutability | Immutable | Mutable (via setter) |
| Who controls it? | Parent component | Component itself |
| Where is it defined? | Passed from parent | Declared inside the component |
| Purpose | Configure component | Manage dynamic data |
| Use when | You need to pass data down | You need to change data over time |
Lifting State Up
Sometimes two sibling components need to share the same data. The React way is to lift the state up to their closest common ancestor.
function Parent() {
const [sharedValue, setSharedValue] = useState('');
return (
<div>
<ChildA value={sharedValue} onChange={setSharedValue} />
<ChildB value={sharedValue} />
</div>
);
}
function ChildA({ value, onChange }) {
return (
<input
value={value}
onChange={(e) => onChange(e.target.value)}
/>
);
}
function ChildB({ value }) {
return <p>Current value: {value}</p>;
}
Now both children react to the same state, and changes in ChildA automatically update ChildB.
Practical Example – A Simple Todo List
Let’s combine state and props to build a working todo list.
import { useState } from 'react';
function TodoApp() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const addTodo = () => {
if (input.trim()) {
setTodos([...todos, { id: Date.now(), text: input, completed: false }]);
setInput('');
}
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
return (
<div>
<h1>My Todo List</h1>
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add a task"
/>
<button onClick={addTodo}>Add</button>
</div>
<TodoList todos={todos} onToggle={toggleTodo} />
</div>
);
}
function TodoList({ todos, onToggle }) {
return (
<ul>
{todos.map(todo => (
<li
key={todo.id}
onClick={() => onToggle(todo.id)}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
{todo.text}
</li>
))}
</ul>
);
}
TodoAppmanages the state for todos and the input.It passes
todosand a handler (onToggle) down toTodoListas props.TodoListrenders the list and callsonTogglewhen an item is clicked.
Conclusion
Props and state are the two fundamentals that make React apps interactive and maintainable. Remember these core ideas:
Props: read‑only inputs a parent passes to a child — use them to configure components and send data or callbacks downward.
State: local, mutable data a component manages with hooks like
useState— use it for UI that changes over time (form values, toggles, fetched data).Rule of thumb: if multiple components need the same data, lift state up to the closest common ancestor and pass it down via props (or use Context for broader sharing).
Never mutate props directly; instead, update state in the owner and pass new values down.
Pass functions as props to let children request state changes in their parents (events → callbacks → state updates).
Minimize unnecessary state: derive values from props/state instead of duplicating them.
Address prop‑drilling with Context or state managers when many intermediate components only forward props.
Keep performance in mind: memoize pure child components with
React.memoand use hooks likeuseCallbackanduseMemowhen appropriate.
Next steps: try a small exercise — build a parent component that manages a list (state), renders child item components (props), and lets children remove or update items via callback props. It’ll reinforce how props and state work together in real apps.
Bonus Challenge: Extend the todo list with a delete button for each item. Use the same pattern – pass a delete handler down via props.

