Lifting State Up

Difficulty: Intermediate

Lifting state up is a pattern where you move state from a child component to their closest common parent so that multiple children can share and synchronize the same data. This is necessary because React's data flow is unidirectional - data only flows downward from parent to child via props. When two sibling components need to reflect the same changing data, the state must live in their parent.

Consider a temperature converter with Celsius and Fahrenheit inputs. If each input manages its own state independently, they cannot stay in sync. The solution is to lift the temperature state up to the parent component. The parent holds the single source of truth and passes the current value down to both inputs as props. When either input changes, it calls a callback prop (like onChange) that the parent provided, and the parent updates the state, causing both inputs to re-render with the new synchronized value.

The 'inverse data flow' part of this pattern refers to the child-to-parent communication. Since children cannot modify parent state directly, the parent passes a callback function as a prop. The child calls this function when something changes (like a user typing in an input). The parent's callback updates the parent's state, which flows back down as props. This completes the cycle: state flows down as props, events flow up as callbacks.

Deciding where state should live follows a simple process: identify every component that renders something based on that state. Find their closest common ancestor - that is where the state should live. If no common ancestor makes sense, you can create a new component solely to hold the state and place it above the others. This principle is called the 'single source of truth' - each piece of state should be owned by exactly one component.

Lifting state has a cost: the parent component becomes more complex, and you end up passing props through more layers. If lifting state leads to prop drilling (passing props through many intermediate components that do not use them), consider using React Context or a state management library. However, for most cases involving a few closely related components, lifting state up is the simplest and most appropriate pattern.

Code examples

Lifting State to Share Between Siblings

// Two children need to share the same count
function DisplayCount(props) {
  return 'Count is: ' + props.count;
}

function Controls(props) {
  // Child communicates up via callback
  props.onIncrement();
  return 'Incremented!';
}

// Parent holds the shared state
function App() {
  let count = 0;
  const increment = () => { count += 1; };

  // Before interaction
  console.log(DisplayCount({ count }));

  // Child triggers state change
  console.log(Controls({ onIncrement: increment }));

  // After interaction - state is shared
  console.log(DisplayCount({ count }));
}

App();

The parent App holds the count state. DisplayCount reads it via props, and Controls updates it via a callback prop. Both siblings see the same data.

Temperature Converter Pattern

function toCelsius(f) { return ((f - 32) * 5) / 9; }
function toFahrenheit(c) { return (c * 9) / 5 + 32; }

// Parent holds the single source of truth
function TemperatureConverter() {
  let temperature = 0;
  let scale = 'c';

  function handleCelsiusChange(value) {
    temperature = value;
    scale = 'c';
  }

  function handleFahrenheitChange(value) {
    temperature = value;
    scale = 'f';
  }

  // Simulate user entering 100 celsius
  handleCelsiusChange(100);

  const celsius = scale === 'c' ? temperature : toCelsius(temperature);
  const fahrenheit = scale === 'f' ? temperature : toFahrenheit(temperature);

  console.log('Celsius: ' + celsius);
  console.log('Fahrenheit: ' + fahrenheit);

  // Simulate user entering 32 fahrenheit
  handleFahrenheitChange(32);

  const c2 = scale === 'c' ? temperature : toCelsius(temperature);
  const f2 = scale === 'f' ? temperature : toFahrenheit(temperature);

  console.log('Celsius: ' + c2);
  console.log('Fahrenheit: ' + f2);
}

TemperatureConverter();

The parent stores the temperature and scale as state. Both inputs derive their display value from this single source of truth. Changing one input updates both.

Finding the Right Component for State

// Scenario: SearchBar and ResultsList both need the search query

function SearchBar(props) {
  // Calls parent's onSearch when user types
  return 'SearchBar: query="' + props.query + '"';
}

function ResultsList(props) {
  const filtered = props.items.filter(item =>
    item.toLowerCase().includes(props.query.toLowerCase())
  );
  return 'Results: ' + filtered.join(', ');
}

// Parent holds query state - the closest common ancestor
function SearchPage() {
  const items = ['React', 'Redux', 'Router', 'Angular', 'Vue'];
  let query = '';

  function handleSearch(newQuery) {
    query = newQuery;
  }

  // Simulate typing 'Re'
  handleSearch('Re');

  console.log(SearchBar({ query }));
  console.log(ResultsList({ items, query }));
}

SearchPage();

The search query state lives in SearchPage because both SearchBar (to show current value) and ResultsList (to filter) need access to it.

Key points

Concepts covered

lifting state, shared state, inverse data flow, single source of truth, state management patterns