DEV Community

Cover image for Mastering SOLID Principles in React: Elevating Your Code Quality
Vishal Yadav
Vishal Yadav

Posted on

Mastering SOLID Principles in React: Elevating Your Code Quality

When it comes to developing robust, maintainable, and scalable React applications, applying SOLID principles can be a game-changer. These object-oriented design principles provide a strong foundation for writing clean and efficient code, ensuring that your React components are not only functional but also easy to manage and extend.

In this blog, we'll dive into how you can apply each of the SOLID principles to your React development, complete with code examples to illustrate these concepts in action.


1. Single Responsibility Principle (SRP)

Definition: A class or component should have only one reason to change, meaning it should focus on a single responsibility.

In React: Each component should handle a specific piece of functionality. This makes your components more reusable and easier to debug or update.

Example:

// UserProfile.js
const UserProfile = ({ user }) => (
  <div>
    <h1>{user.name}</h1>
    <p>{user.bio}</p>
  </div>
);

// AuthManager.js
const AuthManager = () => (
  <div>
    {/* Authentication logic here */}
    Login Form
  </div>
);
Enter fullscreen mode Exit fullscreen mode

In this example, UserProfile is responsible solely for displaying the user profile, while AuthManager handles the authentication process. Keeping these responsibilities separate follows the SRP, making each component easier to manage and test.


2. Open/Closed Principle (OCP)

Definition: Software entities should be open for extension but closed for modification.

In React: Design components that can be extended with new functionality without modifying existing code. This is crucial for maintaining stability in large-scale applications.

Example:

// Button.js
const Button = ({ label, onClick }) => (
  <button onClick={onClick}>{label}</button>
);

// IconButton.js
const IconButton = ({ icon, label, onClick }) => (
  <Button label={label} onClick={onClick}>
    <span className="icon">{icon}</span>
  </Button>
);
Enter fullscreen mode Exit fullscreen mode

Here, the Button component is simple and reusable, while the IconButton extends it by adding an icon, without altering the original Button component. This adheres to the OCP by allowing extension through new components.


3. Liskov Substitution Principle (LSP)

Definition: Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.

In React: When creating components, ensure that derived components can seamlessly replace their base components without breaking the application.

Example:

// Button.js
const Button = ({ label, onClick, className = '' }) => (
  <button onClick={onClick} className={`button ${className}`}>
    {label}
  </button>
);

// PrimaryButton.js
const PrimaryButton = ({ label, onClick, ...props }) => (
  <Button label={label} onClick={onClick} className="button-primary" {...props} />
);

// SecondaryButton.js
const SecondaryButton = ({ label, onClick, ...props }) => (
  <Button label={label} onClick={onClick} className="button-secondary" {...props} />
);
Enter fullscreen mode Exit fullscreen mode

PrimaryButton and SecondaryButton extend the Button component by adding specific styles, but they can still be used interchangeably with the Button component. This adherence to LSP ensures that the application remains consistent and bug-free when substituting these components.


4. Interface Segregation Principle (ISP)

Definition: Clients should not be forced to depend on methods they do not use.

In React: Create smaller, more specific interfaces (props) for your components instead of one large, monolithic interface. This ensures that components only receive the props they need.

Example:

// TextInput.js
const TextInput = ({ label, value, onChange }) => (
  <div>
    <label>{label}</label>
    <input type="text" value={value} onChange={onChange} />
  </div>
);

// CheckboxInput.js
const CheckboxInput = ({ label, checked, onChange }) => (
  <div>
    <label>{label}</label>
    <input type="checkbox" checked={checked} onChange={onChange} />
  </div>
);

// UserForm.js
const UserForm = ({ user, setUser }) => {
  const handleInputChange = (e) => {
    const { name, value } = e.target;
    setUser((prevUser) => ({ ...prevUser, [name]: value }));
  };

  const handleCheckboxChange = (e) => {
    const { name, checked } = e.target;
    setUser((prevUser) => ({ ...prevUser, [name]: checked }));
  };

  return (
    <>
      <TextInput label="Name" value={user.name} onChange={handleInputChange} />
      <TextInput label="Email" value={user.email} onChange={handleInputChange} />
      <CheckboxInput label="Subscribe" checked={user.subscribe} onChange={handleCheckboxChange} />
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

In this example, TextInput and CheckboxInput are specific components with their own props, ensuring that UserForm only passes the necessary props to each input, following the ISP.


5. Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.

In React: Use hooks and context to manage dependencies and state, ensuring that components are not tightly coupled to specific implementations.

Example:

Step 1: Define an Authentication Service Interface

// AuthService.js
class AuthService {
  login(email, password) {
    throw new Error("Method not implemented.");
  }
  logout() {
    throw new Error("Method not implemented.");
  }
  getCurrentUser() {
    throw new Error("Method not implemented.");
  }
}
export default AuthService;
Enter fullscreen mode Exit fullscreen mode

Step 2: Implement Specific Authentication Services

// FirebaseAuthService.js
import AuthService from './AuthService';

class FirebaseAuthService extends AuthService {
  login(email, password) {
    console.log(`Logging in with Firebase using ${email}`);
    // Firebase-specific login code here
  }
  logout() {
    console.log("Logging out from Firebase");
    // Firebase-specific logout code here
  }
  getCurrentUser() {
    console.log("Getting current user from Firebase");
    // Firebase-specific code to get current user here
  }
}

export default FirebaseAuthService;
Enter fullscreen mode Exit fullscreen mode
// AuthOService.js
import AuthService from './AuthService';

class AuthOService extends AuthService {
  login(email, password) {
    console.log(`Logging in with AuthO using ${email}`);
    // AuthO-specific login code here
  }
  logout() {
    console.log("Logging out from AuthO");
    // AuthO-specific logout code here
  }
  getCurrentUser() {
    console.log("Getting current user from AuthO");
    // AuthO-specific code to get current user here
  }
}

export default AuthOService;
Enter fullscreen mode Exit fullscreen mode

Step 3: Create the Auth Context and Provider

// AuthContext.js
import React, { createContext, useContext } from 'react';

const AuthContext = createContext();

const AuthProvider = ({ children, authService }) => (
  <AuthContext.Provider value={authService}>
    {children}
  </AuthContext.Provider>
);

const useAuth = () => useContext(AuthContext);

export { AuthProvider, useAuth };
Enter fullscreen mode Exit fullscreen mode

Step 4: Use the Auth Service in the Login Component

// Login.js
import React, { useState } from 'react';
import { useAuth } from './AuthContext';

const Login = () => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const authService = useAuth();

  const handleLogin = () => {
    authService.login(email, password);
  };

  return (
    <div>
      <h1>Login</h1>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Enter email"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Enter password"
      />
      <button onClick={handleLogin}>Login</button>
    </div>
  );
};

export default Login;
Enter fullscreen mode Exit fullscreen mode

Step 5: Integrate the Provider in the App

// App.js
import React from 'react';
import { AuthProvider } from './AuthContext';
import FirebaseAuthService from './FirebaseAuthService';
import Login from './Login';

const authService = new FirebaseAuthService();

const App = () => (
  <AuthProvider authService={authService}>
    <Login />
  </AuthProvider>
);

export default App;
Enter fullscreen mode Exit fullscreen mode

Benefits of Applying DIP in React:

  1. Decoupling: High-level components (like Login) are decoupled from low-level implementations (like FirebaseAuthService and AuthOService). They depend on an abstraction (AuthService), making the code more flexible and easier to maintain.
  2. Flexibility: Switching between different authentication services is straightforward. You only need to change the implementation passed to the AuthProvider without modifying the Login component.
  3. Testability: The use of abstractions makes it easier to mock services in tests, ensuring that

components can be tested in isolation.


Conclusion

Implementing SOLID principles in React not only elevates the quality of your code but also improves the maintainability and scalability of your application. Whether you're building a small project or a large-scale application, these principles serve as a roadmap to clean, efficient, and robust React development.

By embracing SOLID principles, you create components that are easier to understand, test, and extend, making your development process more efficient and your applications more reliable. So, next time you sit down to code in React, remember these principles and see the difference they make!

Top comments (19)

Collapse
 
dmeskhi profile image
David Meskhishvili

I'm covering this topic at Odin Project right now and just came across this article. Thank you.

Collapse
 
vyan profile image
Vishal Yadav

Cool

Collapse
 
ben_ngayo profile image
Ngayo

Doing the same same thing as well

Collapse
 
mathiasfc profile image
Mathias Falci

Really like your React examples, thanks for sharing 🙌

And it's absolutely true: "Elevates the quality of your code ... Whether you're building a small project or a large-scale application..."

It's a great feeling when after months or even years we revisit our well-written code :)

Collapse
 
devtobinaryoperations profile image
Shashank

The open closed example doesn't work. Button doesn't get render the children prop and the the icon of never displayed.

Otherwise really good examples. Especially the dependency injection principle.

Collapse
 
rishadomar profile image
Rishad Omar

Thanks. I appreciate the good examples.

Collapse
 
vyan profile image
Vishal Yadav

Welcome!

Collapse
 
ayik_bastian_e059c4c10dcc profile image
Ayik Bastian

Thakks

Collapse
 
vyan profile image
Vishal Yadav

Nice

Collapse
 
nakjemmy profile image
Nsiah Akuoko Jeremiah

Great content. Thank you.

Collapse
 
vyan profile image
Vishal Yadav

Nice

Collapse
 
kibobishtrudelz profile image
Petar Kolev

Thanks for the great article!

Collapse
 
ricardogesteves profile image
Ricardo Esteves

Nice, great article @vyan !

Collapse
 
jwp profile image
John Peters

Excellent article too bad Javascript people hate SOLID. It's been working for over 20 years and can be used in Any language. Your example of Open Closed is very creative...

Collapse
 
aroldev profile image
Arol

This is really cool and great examples. Thank you!!

Collapse
 
dove-white profile image
Redeemer Dugbazah

Really helpful 👌

Collapse
 
jsinnerx profile image
José Gabriel Gonzålez Pérez

đŸ€˜

Collapse
 
mohnatus profile image
Furry Cat

It would also be great to see bad examples for these principles to understand how not to do

Collapse
 
landrian61 profile image
Luswata Andrew

Can the SOLID principles also be applied in react-native?