React props and state management can be confusing. Learn the right way to structure your data, avoid prop drilling, and keep your components predictable.

We’ve all been there: you’re deep into a component refactor, passing a userObject through five layers of components just to display a name in a footer. It feels messy, it breaks easily, and you realize you’ve created a maintenance nightmare.
The core of building scalable React applications isn't just knowing how to write useState or useEffect; it's mastering React props and state management. If you don't decide where your data lives early, your component tree will quickly become a tangled web of dependencies.
In React, data should flow one way: down. When you're deciding where a piece of state should live, ask yourself a simple question: "Does more than one component need to see this?"
If the answer is no, keep it local. If the answer is yes, you need to lift it up to the nearest common ancestor.
We once tried to manage form input state inside a deeply nested SubmitButton component. It worked for the initial prototype, but as soon as the design team asked for a "Reset" button in the header, the entire logic fell apart. We had to rewrite the state management entirely. Lesson learned: don't bury your state.
Think of props as the arguments you pass to a function, and state as the variables defined inside that function.
When you're working with React 19 upgrades: What you actually need to know for production, you’ll find that cleaner state management helps you leverage new features like the use hook and improved server-side integration much more effectively.

When you’re stuck deciding where data belongs, use this hierarchy:
useState): Use this for UI-specific things like toggling a dropdown or tracking a text input.Here is a common scenario—a simple counter shared between two components:
JSX// The wrong way: duplicating state in both components // The right way: Lift the state to the parent function Dashboard() { const [count, setCount] = useState(0); return ( <div> <Display count={count} /> <Controls increment={() => setCount(c => c + 1)} /> </div> ); }
By keeping the state in Dashboard, both Display and Controls remain "dumb" or presentational components. They don't care how the count changes; they just receive the data they need via props.
Sometimes, you'll encounter a situation where you need data from an API. If you are extending the WordPress REST API with custom endpoints, remember that your frontend shouldn't be responsible for transforming that raw data.
Fetch the data in a top-level component, keep it in state, and pass it down as props. If you start trying to fetch data inside every leaf node, your app will suffer from "waterfall" loading issues, causing performance to tank—often adding around 300ms to 500ms of unnecessary latency per component request.
Context is great for global settings like themes or user authentication. If you put everything in Context, you'll trigger re-renders across your entire app every time a single value changes. Keep your state as local as possible.
If you find yourself passing a prop through a component that doesn't actually use it—just to get it to a child—you're prop drilling. It’s a sign that your component composition needs a rethink.
useEffect is for syncing your state with external systems (like an API or the DOM). Don't use it to sync state between two variables inside your component; that's almost always a sign that you should just derive the value during render instead.

I’m still refining my own approach to state. Sometimes I hold onto local state for too long because I'm lazy, only to pay the price when I have to refactor it later. Don't be afraid to pull state up the tree as soon as you realize a second component needs access to it. It feels like extra work in the moment, but your future self—and your code—will thank you for it.
Profiling and fixing a slow React render is easier when you stop guessing. Learn how to use React DevTools to find bottlenecks and optimize your app.