Skip to main content

Single Source of Truth: State Ownership & Lifting

The Single Source of Truth (SSOT) principle states that each piece of data should be owned and managed by exactly one component. This eliminates bugs caused by duplicate, out-of-sync state and is the foundation of predictable React applications. The key is finding the closest common ancestor of all components that need the data.

Key Takeaways

  • A single component should own any given piece of state; multiple components reading the same state should receive it as a prop from the owner
  • Find the owner by locating the closest common ancestor of all components that need to read or update the state
  • Duplicate state in different components causes the UI to become inconsistent and bugs to accumulate
  • Keep state as local as possible; only lift it when you have a clear need to share it between sibling or distant components

What is a Single Source of Truth?

In any application, you have data that changes over time: the items in a shopping cart, whether a user is logged in, the text in a search field, the number of unread notifications. The Single Source of Truth principle states that the state for each piece of data should live in only one place.

Why is this critical?

Imagine you have two components that both display the number of items in a shopping cart. If each component managed its own cartItemCount state, what would happen?

  • If a user adds an item, you would have to remember to update the state in both components
  • If you forget to update one, the UI becomes inconsistent and displays conflicting information—a common source of bugs
  • As the application grows, this problem gets exponentially worse. A feature might work in one component but not another
  • Testing becomes harder because you have to synchronize state across multiple locations

By establishing a single source of truth, you eliminate this entire class of problems. There is only one cartItemCount state, and any component that needs to display it receives it from that single, authoritative source.

Identifying the State's "Owner"

The key to implementing a single source of truth is to identify the correct "owner" for each piece of state. The owner is the component that will manage (own) the state and pass it down as props to children.

To find the owner, ask yourself:

  1. Which components need to read this state?
  2. Which components need to update this state?
  3. Find the closest common ancestor of all these components in the component tree

This common ancestor is the perfect place to own the state. It can then pass the state down as props to all components that need to read it, and pass callback functions down to allow updates.

Project-Based Example: A Shopping Cart UI

Let us consider a simple e-commerce UI. We have a list of products and a "cart summary" in the header that shows how many items are in the cart.

The Problem: Two Sources of Truth

Here is how the application might look if we do not follow the SSOT principle:

// This is an example of what NOT to do.

// ProductList.jsx
function ProductList() {
const [cartItems, setCartItems] = useState([]);
// ... logic to add items to cartItems ...
return <div>{/* ... product list ... */}</div>;
}

// CartSummary.jsx
function CartSummary() {
const [cartItemCount, setCartItemCount] = useState(0);
// ... how does this get updated? ...
return <div>Cart: {cartItemCount} items</div>;
}

// App.jsx
function App() {
return (
<div>
<CartSummary />
<ProductList />
</div>
)
}

The ProductList has its own cartItems state, and CartSummary has its own cartItemCount state. They are completely disconnected. How does adding an item in ProductList update the count in CartSummary? It does not. This is a recipe for bugs and inconsistent UIs.

The Solution: Lifting State to a Common Ancestor

The closest common ancestor of ProductList and CartSummary is App. Therefore, App should own the shopping cart state. Both children receive the data as a prop and callback functions to update it.

Let us refactor it correctly:

// App.jsx - The Single Source of Truth

import React, { useState } from 'react';

// Child components would be in their own files
function ProductList({ onAddToCart }) {
const products = ['Apples', 'Oranges', 'Bananas'];
return (
<div>
<h2>Products</h2>
<ul>
{products.map((product, index) => (
<li key={index}>
{product} <button onClick={() => onAddToCart(product)}>Add to Cart</button>
</li>
))}
</ul>
</div>
);
}

function CartSummary({ cartItemCount }) {
return <div>Cart: {cartItemCount} items</div>;
}

function App() {
const [cartItems, setCartItems] = useState([]);

const handleAddToCart = (product) => {
setCartItems([...cartItems, product]);
};

return (
<div>
<CartSummary cartItemCount={cartItems.length} />
<hr />
<ProductList onAddToCart={handleAddToCart} />
</div>
);
}

export default App;

Walkthrough:

  1. State Ownership: The App component now owns the cartItems array. This is the single source of truth.
  2. Data Flows Down:
    • App renders CartSummary and passes it the number of items in the cart (cartItems.length)
    • CartSummary simply displays the number it receives; it has no state of its own
  3. Events Flow Up:
    • App renders ProductList and passes down the handleAddToCart function
    • When a user clicks "Add to Cart" in the ProductList, it calls the handleAddToCart function, which updates the state in the App component
  4. Predictable Re-renders: When the state in App changes, it re-renders, passing the new cartItems.length to CartSummary. The UI is always consistent.

This unidirectional flow (data down, events up) is the heart of React's data model. It guarantees that each component has a single source of truth for every piece of data.

Best Practices for State Ownership

  • Keep State Local First: Do not start by putting all your state in the top-level component. Always start by keeping state as local as possible (in the component that uses it). Only lift it up when you have a clear need to share it between components.
  • SSOT Applies to All State: This principle is not just for complex data. It applies to everything, from simple boolean flags (like isMenuOpen) to user input in forms to asynchronous data. One source of truth per piece of data.
  • Find the Common Ancestor: When deciding where to own state, trace the component tree upward from each component that needs the state. The lowest component that is an ancestor of all of them is the owner.

Frequently Asked Questions

How do I know when to lift state up?

Lift state up when two or more sibling components need to share data or when a parent component needs to coordinate between children. If only one component uses the state, keep it local. If a component and a distant descendant need to share data, lift it to their common ancestor (or consider using Context API for deeply nested trees).

What is the difference between state and props?

State is data owned and managed by a component using useState. Props are data passed from a parent component to a child. State changes cause the component to re-render; props are read-only from the child's perspective. Props enforce a one-way data flow, making the app predictable.

Can I have multiple sources of truth for the same data?

No, that is an anti-pattern. Multiple sources of truth cause inconsistency and bugs. Always ensure that any piece of data has exactly one owner. Other components receive it as a prop or through a callback function.

What if multiple components at different levels need the same state?

Find their closest common ancestor and lift the state there. If the common ancestor is very high in the tree (causing "prop drilling" through many intermediate components), consider using React Context API to pass data without explicitly passing it through every intermediate component's props.

Should I lift state before I need to?

No. Start with state local to the component that uses it. Only lift state when you actually have a need to share it. Lifting prematurely makes your code harder to reason about. React's philosophy is to lift state only when necessary.

Further Reading

Glossary

  • Single Source of Truth (SSOT): A design principle in which the state for any given piece of data is managed in a single, authoritative location within the application.
  • Lifting State Up: The process of moving state from a child component to its parent (or ancestor) so that sibling components can share the data.
  • State Owner: The component that creates and manages a piece of state using useState or useReducer. Other components receive this state as props.