Respecting SOLID principles with React

The SOLID principles are a set of five fundamental programming concepts. They are mostly applied in OOP (Object-Oriented Programming). They can also be applied elsewhere. They are indispensable in software architecture. To present these principles, I've chosen to use Javascript rather than Typescript, so as not to weigh down the examples. We're going to apply these principles to a simple "Todo List".
•
Updated on August 14, 2026
•
6 min read
Believemy logo

What Are the SOLID Principles?

The acronym S.O.L.I.D was coined by Michael Feathers based on the object-oriented programming principles identified by Robert Cecil Martin AKA Uncle Bob (Clean Code Handbook / Clean Coder).

These principles aim to make code more readable (clean), easy to maintain, easy to evolve, reusable, and free from code duplication.

By "easy," it means that the necessary cost to make a change to the application should always be less than the benefits directly provided by that change.

As developers, by following these principles, we can design robust, flexible, extensible, and easier-to-maintain applications.

Applying these principles significantly contributes to having quality code and ensuring better management of the lifecycle of our applications.

Note:
Understanding the 5 SOLID principles and using them will allow us::

  • To improve code quality.
  • To understand more evolved codes.
  • To reduce the technical debt of our projects.

 

Using these principles should become a habit in your daily routine.

 

The "S" for SRP: Single Responsibility Principle

The Single Responsibility Principle states that a class should have only one reason to change, meaning that a class (component, function) should have only one responsibility.

This ensures that a function does only one thing but does it well (cf. SOC: Separation Of Concerns).

Bad Example:
The component manages both display and task management logic.

JSX
const TodoListWidget = () => {
  const [todos, setTodos] = useState([]);

  const fetchTodos = () => {
    fetch('https://api.example.com/todos')
      .then(response => response.json())
      .then(data => setTodos(data))
      .catch(error => console.error(error));
  };

  useEffect(() => {
    fetchTodos(); // Retrieves tasks from an API
  }, []);

  return (
    <div>
      {todos.map(todo => (
        <div key={todo.id}>
          {todo.title}
        </div>
      ))}
    </div>
  );
};

 

Good Example:
Divide the component into two:

  • One for display
  • One for fetching tasks.
JSX
const TodoListWidget = () => {
  const [todos, setTodos] = useState([]);

  const fetchTodos = () => {
    fetch('https://api.example.com/todos')
      .then(response => response.json())
      .then(data => setTodos(data))
      .catch(error => console.error(error));
  };

  useEffect(() => {
    fetchTodos(); // Retrieves tasks from an API
  }, []);

  return (
    <TodoListItems todos={todos} />
  );
};

const TodoListItems = ({ todos }) => {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          {todo.title}
        </li>
      ))}
    </ul>
  );
};


In the bad example, the TodoListWidget component manages both fetching tasks from an API and displaying tasks.

This violates the Single Responsibility Principle because a component should do only one thing.

In the good example, we have divided the TodoListWidget component into two distinct components: TodoListWidget and TodoListItems.

The TodoListWidget component is responsible for fetching tasks from the API and renders the TodoListItems component, which is responsible for displaying tasks.

The code is clearer, more maintainable, and adheres to this principle—each component has a single responsibility.

We can go even further because the TodoListItems component also violates the Single Responsibility Principle as previously seen. Therefore, it's not 100% correct.

 

The "O" for OCP: Open/Closed Principle

The Open/Closed Principle advocates that software entities, such as classes and modules, should be open for extension but closed for modification.

A class or module should be extendable to add new functionalities without modifying its existing source code.

This makes it easier to add new methods without disrupting existing behavior.

Simply put.

We should avoid passing objects as parameters when an interface is available. This ensures that the object we are manipulating, regardless of its type, will have the correct associated methods/types.

This requires passing abstractions as parameters, such as contracts through interfaces (for Object-Oriented languages), rather than the objects themselves.

 

Bad Example:
The TodoListItems component directly handles adding a new task.

JSX
const TodoListWidget = () => {
  const [todos, setTodos] = useState([]);

  const addTodo = (newTodo) => {
    setTodos([...todos, newTodo]);
  };

  return (
    <TodoListItems todos={todos} addTodo={addTodo} />
  );
};

 

Good Example:
Use an external handler function to invert the dependency between TodoListWidget and task retrieval.

JSX
const TodoListWidget = ({ fetchCallback, customListItemProvider }) => {
  const [todos, setTodos] = useState([]);

  const fetchTodos = () => {
    // Code to retrieve tasks from an API or elsewhere
    const todoItems = fetchCallback(); // call to our injected callback function
    setTodos(todoItems);
  };

  const handleAddTodo = (newTodo) => {
    setTodos([...todos, newTodo])
  };
  const handleDeleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id))
  };

  const handleToggleCompleted = (id) => {
    setTodos(todos.map(todo => {
      if (todo.id === id) {
        return {...todo, completed: !todo.completed}
      }
      return todo
    }))
  };

  useEffect(() => {
    fetchTodos()
  }, []);

  return (
    <div>
      <h1>Todo List Widget</h1>
      <hr/>      
      <TodoListItems todos={todos} 
                     deleteTodoHandler={handleDeleteTodo} 
                     toggleTodoCompletedHandler={handleToggleCompleted} 
                     customListItemProvider={customListItemProvider}/>
    </div>
  )
}

export const TodoListItems = ({todos, deleteTodoHandler, toggleTodoCompletedHandler, customListItemProvider}) => {
  return (
    <div>
      {todos.length === 0 && <p className={'text-center'}>No tasks to display</p>}
      {todos.length > 0 &&
        <ul>
          {todos.map((todo) => (
            <li key={todo.id}>
              {customListItemProvider(todo, deleteTodoHandler, toggleTodoCompletedHandler)}
            </li>
          ))}
        </ul>
      }
    </div>
  )
}

export const TodoItem = ({item, deleteTodoHandler, toggleTodoCompletedHandler}) => {
  return (
      <div>
        <input id={`check-${item.id}`} type="checkbox" defaultChecked={item.completed}      
               onClick={() => toggleTodoCompletedHandler(item.id)}/>
        <label htmlFor={`check-${item.id}`}>{item.title}</label>
        <button onClick={() => deleteTodoHandler(item.id)}>X</button>
      </div>
  )
}

And in our App.jsx

JSX
import {TodoListWidget} from "./components/Todolist/TodoListWidget.jsx";
import {getData} from "./data/todoListData.js";
import {getData as getDataExt} from "./data/todoListDataExt.js";
import {TodoItemExt} from "./components/Todolist/TodoListExtended/TodoItemExt.jsx";
import {TodoItem} from "./components/Todolist/TodoItem.jsx";

function App() {

  const todoItemProvider = (todo, deleteTodoHandler, toggleTodoCompletedHandler) => {
    return <TodoItem item={todo} 
                     deleteTodoHandler={deleteTodoHandler} 
                     toggleTodoCompletedHandler={toggleTodoCompletedHandler}/>
  }

  const todoItemExtProvider = (todo, deleteTodoHandler, toggleTodoCompletedHandler) => {
    return <TodoItemExt item={todo} 
                        deleteTodoHandler={deleteTodoHandler} 
                        toggleTodoCompletedHandler={toggleTodoCompletedHandler}/>
  }


  return (
    <div>
      <div">
        <TodoListWidget fetchCallback={getData} customListItemProvider={todoItemProvider}/>
        <TodoListWidget fetchCallback={getDataExt} customListItemProvider={todoItemExtProvider}/>
      </div>
    </div>
  )
}

export default App

You may have noticed in the code above, I removed the addTodoHandler prop from the TodoListItems component. Why? As mentioned earlier, the Single Responsibility Principle was being violated. It's not the role of the TodoListItems component to add a task to our data. Its sole role is to display the list of tasks.

Therefore, we need to create a new TodoAddForm component and use it in the TodoListWidget component to continue adhering to the SOLID principles.

I'll let you make this small change yourself.

 

The Complete Code

Find a complete example of this TodoListWidget respecting the SOLID principles on my Github: react-solid-todolist

 

To Go Further

If you want to delve deeper into this area, check out our React JS training!

 

References

Category: Developers

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.

Believemy logo
Comments (0)

You need to be logged in to comment on this article: log in or sign up.

Read next

How to use localStorage in a NextJs or React application

The aim of this article is to teach you how to use localStorage in a Next.JS or React application. Indeed, there are a few differences with the normal use, in a vanilla application.

December 11, 2024

Code faster with React using these techniques

Learn how to code faster with React thanks to simple but effective techniques used by experienced developers.

December 9, 2024

React vs Angular: differences, advantages and disadvantages in 2025

Find out which of React or Angular would be the best choice for your project by understanding their pros and cons in our comprehensive comparison.

December 9, 2024

Why you should learn React in 2025: all the benefits!

Explore why React remains a must-have choice in 2025 for developers. Discover its advantages, the evolution of its features and why the market continues to prefer it.

December 6, 2024

React vs NextJS: what should you choose for your project in 2025?

Which technology should you use, React or NextJS? Discover their advantages and disadvantages to choose the one best suited to your projects.

December 9, 2024

How to use Server Actions with NextJS

Discover why and how NextJS Server Actions improve performance on both Client Components and Server Components, and learn how to use them in your projects.

December 9, 2024

5 must-know methods for JavaScript ES 2023

Discover the 5 must-have methods in JavaScript ES 2023. Boost your skills with the latest ECMAScript features like findLast(), findLastIndex(), and much more.

December 9, 2024

What's new in React 19

React 19 has just been announced by the Meta team. Powerful new features have been introduced almost two years after the release of React 18.3. Let's find out what's new with React 19.

December 5, 2024

Become the solopreneur you dream of being.

First project, career change, business launch: access our courses, practice on real-world cases, and move forward with clear guidance.