What is Lifting State up in ReactJS

What is Lifting State up in ReactJS

An essential pattern to know in react when you have multiple components with shared state.

React has a unidirectional flow of data. Unidirectional flow simply means that the data can flow in only one direction. So, in react, the data can flow from a parent to child component and cannot flow from child to parent.

In our applications, if we want to communicate a state change in a child component to its parent component, we cannot do that in react. In such situations, we can Lift the state up from the child component and move it inside the parent component.

This article assumes that you've worked with React and have experience with simple state management (useState hook), components, and props.

In this article, we will look into the following:

  1. What is lifting the state up.
  2. Why should we lift the state up with example
  3. Where I used it
  4. Real-world example (where it could have been used)
  5. Conclusion.

What is lifting the state up

Consider we have two components A and B.

  1. A - parent component
  2. B - child component.

Here, component B has a variable called value as a state variable in it.

Untitled (1).jpg

If we need to make some changes in component A, based on the state changes happening in component B, we can lift the state up.

Removing the state variable from the child component and moving it into the immediate ancestor is called lifting the state up. We can send the value as props from the parent component to the child component.

Why should we lift the state up with an example

For this part of the article, let us create a simple counter app to see in detail why the state needs to be lifted up. Our counter app has two components

  1. CounterApp - parent component.
  2. Increment - child component.

The counter value and the button to increment the counter is inside the child component and the parent component is just a wrapper. (for now)

// State in the child component
function CounterApp() {
    return <div>
        <h1>Parent component</h1>
        <Increment />
    </div>
}

function Increment() {
    const [count, setCount] = useState(0);
    return (
        <div>
            <h4>Child component</h4>
            Value: <span>{count}</span>
            <button onClick={() => setCount(prev => prev + 1)}>Add</button>
        </div>
    );
}

In the above-mentioned example, let's say the requirement changes and now we want to show the count value inside the parent component. In such a situation, you cannot access any state value of the Child component in the parent component.

No worries. We can pass a callback method to the child component and send back the new updated count value to the parent component. I can maintain a copy of the child's state variable in the parent component and change the count value with the help of a callback.

Passing data to parent with callback

const CounterApp = () => {
    // copy of the state variable in the child component
    const [copy, setCopy] = useState(0);
    // callback method to receive new value and change copy
    const onIncrement = (newValue) => setCopy(newValue);

    return (
        <div>
            <h3>
                With callback - State in both child and parent component
            </h3>
            <p>
                Value: <b>{copy}</b>
            </p>
            <IncrementWithCallback onIncrement={onIncrement} />
        </div>
    );
};

const Increment = ({ onIncrement }) => {
    const [count, setCount] = useState(0);
    // useEffect to invoke the callback method and update the copy
    useEffect(() => {
        onIncrement(count);
    }, [count, onIncrement]);

    return <button onClick={() => setCount((prev) => prev + 1)}>
        Add
    </button>;
};

Yay! Now we have successfully passed the data from the child to the parent component. Everything is fine. But wait until your app grows in size and the number of components increases.

As the number of components that depend upon the state of count increases, it will be hard to maintain the app by making sure all the copy state variables are in sync with the original count variable.

This is where the official React docs advises us to follow the "Single source of truth" pattern for any state changes in a react application. You can lift the state up to the nearest ancestor (root or CounterApp component in our case). This way the state is in one place and we can pass the reference of the same state to the descendant or child components.

Lifting state up

// Lifting state up - State in the parent component
const CounterStateLiftedUp = () => {
    // state lifted up to parent component
    const [count, setCount] = useState(0);
    return (
        <div>
            <h3>Lifting state up - State in parent component</h3>
            <p>
                Value: <b>{count}</b>
            </p>
            <IncrementStateLiftedUp setCount={setCount} />
        </div>
    );
};

// This component receives the setter in props
const IncrementStateLiftedUp = ({ setCount }) => {
    return <button onClick={() => setCount((prev) => prev + 1)}>
        Add
    </button>;

};

We removed the state variable from the Increment component and moved it inside the parent component, CounterApp. But the button to increment is inside the child component. So we pass the setter function setCount as a prop to the Increment component, and it can use it like any setter function and change the application state.

That's it! We have used the "Lifting state up" pattern in our React app.

Where I used it

Enough with the Counter App example. Let us look at some real use cases in this section.

A friend of mine, from a different career background, is trying to get his first front-end job, and I try to help him from time to time. Recently, I gave him the assignment to create a Movie explorer app like this one using the TMDB API.

The page should look something like this.

New Project.jpg

We have to show the following things in our app:

  1. List of movies.
  2. Pagination (because the API gives only 20 results per request).
  3. Pagination at both top and bottom of the page.

We split the page into three components

  1. MoviesList - A component to iterate over the movies and render them in a loop
  2. MovieCard - This component shows information about each movie.
  3. Pagination - To navigate to different pages.

Now let us leave the MovieCard component and concentrate on the MovieList and Pagination components which are the parent and child components respectively.

The first version of code he wrote for the project looked like this

MoviesList (parent) component

function MoviesList() {
    const [movies, setMovies] = useState([]);
    const [totalPages, setTotalPages] = useState(0);
    useEffect(() => {
        // fetch the content from API on initial render and populate the movies array
        fetch("https://api.themoviedb.org/3/trending/movie/week?page=1")
            .then(resp => resp.json())
            .then(body => {
                setMovies(body.results);
                setTotalPages(body.total_pages)
            })
    }, []);
    return (
        <div>
            <Pagination pages={totalPages} />
            <div>
                {movies.map((movie) => <MovieCard movie={movie} />)}
            </div>
            <Pagination pages={totalPages} />
        </div>
    )
}

Pagination (child) component

function Pagination({totalPages}) {
    const pageNumbers = Array.from(Array(totalPages+1).keys()).slice(1);
    const [page, setPage] = useState(1);
    const incrementPage = () => {
        setPage((prev) => {
            if (prev - 1 === 0) {
                return prev;
            }
            return prev - 1;
        });
    }
    const decrementPage = () => {
        setPage((prev) => {
            if (prev + 1 > pages) {
                return prev;
            }
            return prev + 1;
        });
    }
    return (
        <div>
            <button onClick={incrementPage}>
                <AiFillLeftCircle size={30} color="#2563EB"/>
            </button>
            <div className=' overflow-x-scroll flex flex-row gap-3'>
                {pageNumbers.map((pageNumber) => {
                return <div
                    onClick={() => setPage(pageNumber)}
                    className={`${pageNumber === page ? "bg-blue" : "bg-gray"}`}
                    key={pageNumber}
                >
                    <span>{pageNumber}</span>
                </div>
                })}
            </div>
            <button onClick={decrementPage}>
                <AiFillRightCircle size={30} color="#2563EB"/>
            </button>
        </div>
    )
}

When my friend showed his code to me for review, I can quickly find an issue with it. The page number in the fetch API call is a constant, but it should be a variable based on the currently selected page.

This cannot fetch content from different pages.

fetch("https://api.themoviedb.org/3/trending/movie/week?page=1")

Whereas, this can. Because the page number is a variable.

fetch(`https://api.themoviedb.org/3/trending/movie/week?page=${page}`)

But how do we pass the page variable, which is inside the child component (Pagination), to the fetch call inside the parent component MoviesList? If there's one thing we learned from this article so far, it is, that react has a unidirectional flow of data and we can't pass data from child to parent.

This is when I introduced my friend to the concept of lifting the state up and after applying the pattern, the resulting code should look like this.

MoviesList (parent) component

function MoviesList() {
    const [movies, setMovies] = useState([]);
    const [totalPages, setTotalPages] = useState(0);

    // The current page number state variable lifted up 
    // from the child component to the parent component
    const [page, setPage] = useState(1);

    useEffect(() => {
        // page number query param is a variable now thanks to string literal.
        fetch(`https://api.themoviedb.org/3/trending/movie/week?page=${page}`)
            .then(resp => resp.json())
            .then(body => {
                setMovies(body.results);
                setTotalPages(body.total_pages)
            })
    }, []);
    return (
        <div>
            <Pagination page={page} setPage={setPage} totalPages={totalPages} />
            <div>
                {movies.map((movie) => <MovieCard movie={movie} />)}
            </div>
            {/**
            * In addition to the total pages, we would pass two more props
            * 1. currentPageNumber
            * 2. page number setter
            */}
            <Pagination page={page} setPage={setPage} totalPages={totalPages} />
        </div>
    )
}

Pagination (child) component

// page and setPage are now being passed as props from the parent component
function Pagination({page, setPage, totalPages}) {
    const pageNumbers = Array.from(Array(totalPages+1).keys()).slice(1);
    const incrementPage = () => {
        setPage((prev) => {
            if (prev - 1 === 0) {
                return prev;
            }
            return prev - 1;
        });
    }
    const decrementPage = () => {
        setPage((prev) => {
            if (prev + 1 > pages) {
                return prev;
            }
            return prev + 1;
        });
    }
    return (
        <div>
            <button onClick={incrementPage}>
                <AiFillLeftCircle size={30} color="#2563EB"/>
            </button>
            <div className=' overflow-x-scroll flex flex-row gap-3'>
                {pageNumbers.map((pageNumber) => {
                return <div
                    onClick={() => setPage(pageNumber)}
                    className={`${pageNumber === page ? "bg-blue" : "bg-gray"}`}
                    key={pageNumber}
                >
                    <span>{pageNumber}</span>
                </div>
                })}
            </div>
            <button onClick={decrementPage}>
                <AiFillRightCircle size={30} color="#2563EB"/>
            </button>
        </div>
    )
}

Here, we removed the page state variable from the child component and moved it to the parent component just because we wanted to use this value in the parent component.

// removed from Pagination and moved into MoviesList component
const [page, setPage] = useState(1);

The Pagination child component will now receive the state variable and the setter function as props

// page and setPage sent from the parent component as props
export default function Pagination({page, setPage}) {
// react code
}

Real-world example (where it could have been used)

If even this is not enough example, I have one more example (with code). I have noticed a need for this pattern in the shopping cart pages of eCommerce apps. Let us use the Shopping Cart page of the AJIO mobile website as an example.

A production app like AJIO serving millions of customers each day would surely use some sort of state management libraries along with client-side data persistence and caching (especially for the cart) and syncing changes to the cloud based on the user's auth state.

Well for the sake of the article, let us forget about all those things and assume that they have built this page using only useState hook.

Screenshot 2022-11-13 at 6.12.11 PM (1).png

This page is built with two primary components components

  1. ShoppingCart - Entire page - Parent component
  2. CartItem - Each item in the list - Child component

If you look at this highly unrealistic list of products (I mean who buys three sunglasses in the same shade) in this AJIO shopping cart, each item in the list can have its independent quantity.

So, the CartItem component should have its own state variable called quantity. Let us not worry about the size dropdown for the sake of this article

But if you pay enough attention to the page, there are two more important components as well.

  1. TotalItemsInBag - at the top which says the total number of items in the bag
  2. TotalCostOfItems - at the bottom with the total cost of the items in the cart.

To make these two values change in real time as the user changes the quantity of the items, we need two more state variables

  1. totalItems - inside TotalItemsInBag component.
  2. totalCost - inside TotalCostOfItems component.

So, the component hierarchy would look like this

Untitled (2)-min.jpg

So if we declare the state variables like this in each of the components individually, there is no way we can control them. To have better control over the state of the application and a "Single source of truth" for the application state, we need to lift the state up from all the child components and take it back to the nearest ancestor, which is the ShoppingCart component.

const [cart, setCart] = useState([defaultCartItems])

and the data structure of the items in the cart should be like this

const defaultCartItems = [
    {
        name: "Buffalo",
        description: "Belt with Buckle Closure",
        mrp: 399.00,
        offer: 20,
        offerPrice: 319.00,
        quantity: 2,
    },
    {
        name: "Resist Eyewear",
        description: "UV Protected Rimless Wayfarers",
        mrp: 5999.00,
        offer: 74,
        offerPrice: 1560.00,
        quantity: 3,
    },
    {
        name: "Buffalo",
        description: "Belt with Buckle Closure",
        mrp: 9995.00,
        offer: 0,
        offerPrice: 9995.00,
        quantity: 1,
    }
]

The TotalItemsInBag and TotalCostOfItems components will receive this cart array in the props and will calculate the respective values using the following functions (with the magic of array methods).

function getTotalItemsInBag(cart) {
    return cart.reduce((total, item) => total + item.quantity, 0)
}

function getTotalCostOfItems(cart) {
    return cart.reduce((total, item) => total + (item.quantity * item.offerPrice), 0)
}

So the new component hierarchy with the state lifted up would be like this

ajio-good-approach.jpg

I have created a simple version of this AJIO cart page following the "Lifting state up" and "Single source of truth" concepts and using awesome tools like Vite and TailwindCSS.

Conclusion

The need to access the state of a child component is a pretty common use case and Lifting the state up is the solution for that. This is why it is a very important pattern for every React developer to know.

Instead of implementing a state management library like redux or even React Context just because your parent needs the child's state, we can simply lift the state up to the nearest common ancestor and pass the state variable as well as the setter callback as props to the children.

I would like to know from all the awesome react developers, the situation in which you lifted your state up or any component in a production app that you think might have used/need the concepts of "Lifting state up" and "Single source of truth".

P. S. If you think the cover image is cool, go check out CoverView by Rutik