Skip to main content

Lifting State Up: The Solution (Part 2) #59

📖 Introduction

In our previous article, we introduced the foundational theory of Lifting State Up using a temperature converter example. We saw how moving state to a common ancestor allows us to synchronize sibling components. Now, we will solidify that knowledge with a more practical, real-world example that showcases inverse data flow and how to manage a collection of stateful items.


📚 Prerequisites

Before we begin, please ensure you have a solid grasp of the following concepts:

  • The core principles of Lifting State Up
  • Working with arrays in JavaScript (specifically the .map() method)
  • Creating controlled components with forms

🎯 Article Outline: What You'll Master

In this article, you will learn:

  • Practical Application: Building a complete To-Do List application from scratch.
  • Inverse Data Flow: Understanding how child components can communicate with their parents to update state.
  • Managing Collections: How to lift state up when dealing with arrays of objects.
  • Component Composition: Structuring an application with multiple, single-responsibility components.

🧠 Section 1: Recap: The Core Principles

Let's quickly recap the core ideas of lifting state up:

  1. Single Source of Truth: There should be only one component that "owns" and manages a specific piece of state.
  2. Data Flows Down: The owner component passes the state down to its children via props.
  3. Events Flow Up: Children components use callback functions (passed as props) to notify the parent of any required state changes. This is inverse data flow.

In this article, we'll focus heavily on that third point.


💻 Section 2: Structuring Our To-Do List Application

We will build a simple To-Do list application with two main parts:

  1. An input form to add new tasks.
  2. A list to display the existing tasks.

Our component tree will look like this:

App (Owns the `tasks` array state)
├── AddTaskForm (Receives a function to add a new task)
└── TaskList (Receives the `tasks` array to display)

The App component will be our common ancestor. It will own the array of tasks.


🛠️ Section 3: Project-Based Example: Building the To-Do List

Let's build the application step-by-step.

3.1 - The TaskList Component

First, let's create the component responsible for rendering the list of tasks. This is a simple "presentational" component. It receives the tasks array as a prop and renders it.

// TaskList.jsx

import React from 'react';

function TaskList({ tasks }) {
return (
<ul>
{tasks.map((task, index) => (
<li key={index}>{task}</li>
))}
</ul>
);
}

export default TaskList;

Code Breakdown:

  • It receives tasks as a prop.
  • It uses the .map() method to iterate over the tasks array and render a <li> for each one.
  • We use the index as a key for this simple example, though in a real application, a unique ID would be better.

3.2 - The AddTaskForm Component

Next, the form component. This component will be responsible for capturing user input, but it will not add the task to the list itself. Instead, it will call a function passed down from its parent. This is inverse data flow in action.

// AddTaskForm.jsx

import React, { useState } from 'react';

function AddTaskForm({ onAddTask }) {
const [newTask, setNewTask] = useState('');

const handleSubmit = (e) => {
e.preventDefault();
if (newTask.trim() === '') return; // Don't add empty tasks
onAddTask(newTask);
setNewTask(''); // Clear the input after adding
};

return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={newTask}
onChange={(e) => setNewTask(e.target.value)}
placeholder="Add a new task"
/>
<button type="submit">Add</button>
</form>
);
}

export default AddTaskForm;

Code Breakdown:

  • It has its own local state, newTask, to control the input field.
  • It receives a function onAddTask as a prop from its parent.
  • When the form is submitted, it calls onAddTask(newTask), passing the new task's text up to the parent.
  • It then clears its local input state.

3.3 - The App Component: The Single Source of Truth

Finally, let's create the App component that owns the state and brings everything together.

// App.jsx

import React, { useState } from 'react';
import TaskList from './TaskList';
import AddTaskForm from './AddTaskForm';

function App() {
const [tasks, setTasks] = useState(['Learn React', 'Build a project']);

const handleAddTask = (taskText) => {
setTasks([...tasks, taskText]);
};

return (
<div>
<h1>My To-Do List</h1>
<AddTaskForm onAddTask={handleAddTask} />
<TaskList tasks={tasks} />
</div>
);
}

export default App;

Walkthrough:

  1. State Ownership: The App component initializes and owns the tasks state using useState. This is our single source of truth.
  2. The Callback Function: It defines the handleAddTask function. This function knows how to update the tasks state.
  3. Passing the Callback Down: It renders AddTaskForm and passes handleAddTask down as the onAddTask prop.
  4. Passing State Down: It renders TaskList and passes the tasks array down as a prop.

When a user types in the AddTaskForm and clicks "Add", the onAddTask function is called, which is actually the handleAddTask function in the App component. The App component's state is updated, causing it to re-render, which in turn causes TaskList to re-render with the new list of tasks.


💡 Conclusion & Key Takeaways

This To-Do list example is a quintessential demonstration of lifting state up in a practical application.

Let's summarize the key takeaways:

  • Inverse Data Flow is Key: The AddTaskForm component communicated with its parent App component by calling a function passed down as a prop. This is how children can trigger state changes in their ancestors.
  • Component Responsibilities are Clear: The App component is responsible for managing state. The TaskList is responsible for displaying data. The AddTaskForm is responsible for gathering user input. This separation of concerns makes the application easier to understand and maintain.
  • Lifting State is Scalable: This pattern can be applied to much more complex applications with many levels of components.

Challenge Yourself: Try to add a "Remove" button next to each task in the TaskList component. You will need to:

  1. Pass another callback function from App down to TaskList.
  2. This function should accept the index of the task to remove.
  3. In TaskList, when the "Remove" button is clicked, call this function with the correct index.

➡️ Next Steps

You have now seen two practical examples of lifting state up. In the next article, "Creating a Single Source of Truth", we will delve deeper into the design principles behind state management and how to structure your components for maximum clarity and maintainability.

Thank you for your dedication. Stay curious, and happy coding!


glossary

  • Inverse Data Flow: The process by which a child component can pass data or events "up" to a parent component, typically by invoking a callback function passed down as a prop.

Further Reading