- 🔄 Moving state up keeps shared data in one place. This stops data from being copied or clashing across components.
- ⚠️ Passing props through many layers usually means you need global state or a simpler way to manage it.
- 🧠 React's one-way data flow helps state update predictably, which makes apps easier to scale.
- 🛠 Simple libraries like Zustand are becoming more popular than bigger tools like Redux.
- 🚀 Hooks like useCallback and React.memo help fix slow performance issues when you move state up.
If you've worked with React for some time, you've probably heard "lift the state up." But what does it mean, and when should you do it? Understanding this idea is key to writing code that is easy to read and keep up. This is true as your React components grow. This guide explains what moving state up in React means. It also covers when to do it, what problems it can cause, and how it works with other ways to manage state in React.
What Is "Lifting State Up" in React?
In React, "lifting state up" means you move state from a child component to the component closest to them both. This lets sibling components or their children use and change the same state through React's props. This means you have one source for that data.
This technique is a core part of managing state in React. By bringing state together, you stop things from being out of sync. Also, related components stay updated together, even if they are deep in the component tree. Moving state up also makes data changes more clear. This is because you now control state changes from one spot.
Think of it as a way to balance things. You keep state close to where it's used, but high enough that all parts of your UI that need it can get to it and change it.
When Should You Lift State?
Moving state up is not something you always do. It's a choice you make carefully, often because of certain needs in your app. Think about moving state up when:
- Many components need to use the same value.
- Two or more child components need to talk to each other or update right away.
- A parent component needs to react to changes in any of its children.
- Many child components change or use shared data.
Real-World Use Cases
Let's look at some real examples where moving state up works well:
- 📄 Form and Live Preview: You are making a markdown editor. One part lets you type text, and another part shows a preview right away.
- 💬 Chat and Notification Badge: A chat window opens when you click a button in another part of the app. You want a new-message badge to update itself.
- 🔀 Feature Toggle Changes how things look: A toggle button changes themes or layout modes. This then changes how nested parts of the layout and the whole app look.
In each of these, different components need to always get to the same or updated data. Moving that state up to a shared parent is the best way to do it.
Anatomy of Lifting State: Practical Code Examples
Example 1: Shared Counter Between Siblings
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<CounterDisplay count={count} />
<CounterButton onIncrement={() => setCount(count + 1)} />
</>
);
}
function CounterDisplay({ count }) {
return <p>Count: {count}</p>;
}
function CounterButton({ onIncrement }) {
return <button onClick={onIncrement}>Increment</button>;
}
In this setup:
Parenthas thecountstate.CounterDisplayjust shows the value it gets through props.CounterButtonchanges the state using a function it also gets as a prop.
No state is copied, and it's not complex. All components stay small and do one job.
Example 2: Form Synchronization
function FormContainer() {
const [name, setName] = useState("");
return (
<>
<NameInput value={name} onChange={e => setName(e.target.value)} />
<NamePreview name={name} />
</>
);
}
function NameInput({ value, onChange }) {
return <input value={value} onChange={onChange} />;
}
function NamePreview({ name }) {
return <p>Hello, {name}!</p>;
}
The FormContainer component holds the state. It makes sure the input and preview components stay in sync, making the data flow simpler.
How Props and State Work Together
A core part of how React is built is its one-way data flow:
- 🔽 Props go down: Parent components give data to children.
- 🔼 Callbacks go up: Children tell parents about changes using functions passed as props.
- 🎛️ State is local: State starts with
useState. It lives in the components that have the data.
This structure:
Parent (state)
│
├── Child A (receives data via props)
└── Child B (notifies parent via callback)
…gives a lot of clarity and makes things clear. You can follow the data from input to view update. You don't have to worry about unexpected changes.
Common Pitfalls When Lifting State
Moving state up does not work for everything. Using it too much can make code slow or hard to keep up.
1. Lifting Too High in The Tree
Putting state in components that are too far above the components that need it can:
- Cause too many re-renders.
- Add links that are not needed.
- Make debugging harder because components are too closely linked.
2. Prop Drilling is a Problem
If state or callbacks need to be passed four or five levels down, you make long prop chains. This makes components in the middle messy. And then, it also makes changing code harder.
3. Fragmented Modular Design
Sometimes, moving state up makes components depend too much on each other. If it does not make sense to link them closely, this can hurt how easy it is to reuse parts of your code.
Alternatives to Lifting State
When moving state up becomes clunky or too complex, think about other ways to manage state in React.
🌐 React Context API
Good for global values that don't change often, like:
- Themes
- Locales
- Auth/user profiles
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
Good when you want to avoid passing many props but still want one place to get to the state.
⚡ Zustand
Zustand offers a simple way to manage shared state with little extra code. It works well with updates and doesn't need you to wrap components.
const useStore = create(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 }))
}));
Great for:
- Apps built with components
- Projects that grow past just passing props but don't need all the complex parts of Redux.
🧩 Redux
Redux is still useful for:
- Complex ways to change state
- Apps that need middleware or debugging that lets you go back in time
- Global management between many unique components
It uses more code, but Redux gives you very detailed control.
Advanced Techniques: useCallback and Memoization
Moving state up can cause many re-renders that are not needed if you don't make it work well. React re-renders a child when:
- Its parent re-renders
- Its props change
🧠 useCallback
Stops a function from being created again each time the component renders.
const increment = useCallback(() => setCount(c => c + 1), []);
<CounterButton onIncrement={increment} />
🚫 React.memo
Stops child component re-renders if props haven't changed.
const CounterDisplay = React.memo(({ count }) => <p>{count}</p>);
Use them together for the best performance in components nested very deep with shared state.
Prop Drilling: Knowing When It's a Problem
When you find yourself doing:
<Ancestor
someProp={someValue}
onChange={handleChange}
/>
Then:
function Ancestor({ someProp, onChange }) {
return <ChildComponent someProp={someProp} onChange={onChange} />;
}
Then again and again—it's time to think differently.
Ways to fix this include:
- Take state out into a custom hook. Use logic on its own.
- Make a provider context. Move state out of the component tree.
- Design container/presenter components. Keep UI separate from logic.
Designing Components with State in Mind
When you plan how you build your components, think about:
Smart (Stateful) vs Dumb (Presentational)
Smart: Knows about state, manages what the app does
Dumb: Gets props, shows things based on props
function TaskListContainer() {
const [tasks, setTasks] = useState([]);
return <TaskList tasks={tasks} />;
}
function TaskList({ tasks }) {
return tasks.map(task => <li key={task.id}>{task.name}</li>);
}
This makes components easier to reuse and helps apps grow.
Lifting State in Functional Components with Hooks
React hooks gave us an easier way to manage state.
🔧 useState
Makes starting and updating state simpler.
const [value, setValue] = useState('');
🌀 useEffect
Keeps state in sync with:
- APIs
- Browser storage
- Subscriptions
useEffect(() => {
fetchData();
}, []);
Use functional components and hooks for better ways to combine parts when moving state up.
Code Smells That Show You Need to Move State Up
🚩 Warning signs:
- Sibling components acting out of sync
- Doing the same thing in many components
- Many places where the same data lives
- Prop chains that are very deep to handle how things work together
Fix: Move state to the correct common parent or to a global store.
Future-Proofing with State Management That Can Grow
What works for small apps becomes too hard to manage as they grow. Plan how to manage state with this in mind:
- 🧪 Build quick versions and move state up in small apps
- 🔁 Use state logic again with hooks and containers
- 🌐 Use Context or a store (Zustand, Redux) when components interact across many layers
Plan your data flow at the same time you plan how your components are built. Data needs decide how components are built, not the other way around.
Recap and Best Practices
✅ Move state up when:
- Components share or sync the same data
- Data needs to be updated by children and shown in a whole part of the component tree
❌ Don't move state up when:
- Components are not related but linked together by force
- State needs access across the whole app or globally (Context/store is better)
🧪 Quick Checklist:
- Are many components changing the same state?
- Are changes in one component not showing up in another?
- Are you passing props down more than 2 levels?
If yes, it's time to think about moving state up or using a simpler way to manage it.
Finding the Right Way to Simplify
React's strength comes from its simple nature. Patterns like moving state up give it a lot of power. But moving state should fix a problem, not make things complex. Start with small local state. Move it up when it makes sense. And then, only switch to global solutions if you run into problems with things working together or with performance. With smart ways to simplify things—using hooks, smart containers, or light global stores—your app will be able to grow, feel smooth, and be easy to understand.
Citations
- React Documentation. (n.d.). Thinking in React. https://reactjs.org/docs/thinking-in-react.html
- React Documentation. (n.d.). Optimizing Performance. https://reactjs.org/docs/optimizing-performance.html
- State of JavaScript. (2022). State Management Libraries. https://2022.stateofjs.com/en-US/libraries/state-management/
- Airbnb Engineering. (2021). Scaling React Components. https://airbnb.io/frontend/react/architecture