Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Lifting State in React: Are You Doing It Right?

Learn how to lift state and pass variables in React functions. Understand parent-child data flow and event handling best practices.
React lifting state best practices with before and after examples in components layout React lifting state best practices with before and after examples in components layout
  • 🔄 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.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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:

  • Parent has the count state.
  • CounterDisplay just shows the value it gets through props.
  • CounterButton changes 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

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading