DEV Community

Cover image for SOLID Principles: Building Maintainable Applications
Ibrahem ahmad
Ibrahem ahmad

Posted on

SOLID Principles: Building Maintainable Applications

Introduction

SOLID principles are important in React projects, helping create more maintainable, scalable, and readable component-based applications.
Let's see how these principles apply to React with some practical examples.

1. Single Responsibility Principle (SRP)

Every class or function should do one thing and do it well, focusing on a single responsibility.

Real-life Example

💡 A chef shouldn't be responsible for cooking, cleaning, and managing the restaurant. Each task should be assigned to a specific person or team.

Code Example

// Bad Example (Violating SRP)
// Colocation

 const UserCard: React.FC = () => {
    // Multiple responsibilities:
    // 1. Making API call
    // 2. Handling UI updates
  const [user, setUser] = React.useState({ name: '', age: 0 });
  React.useEffect(() => {
    fetch('/api/user')
      .then((res) => res.json())
      .then((data) => setUser(data));
  }, []);

  return <div>{user.name} is {user.age} years old.</div>;
};

// Good Example (Following SRP)
// Separate concerns into different modules

type UserType={ name: string; age: number }

const UserCard: React.FC<{ user: UserType}> = ({ user }) => {
  return <div>{user.name} is {user.age} years old.</div>;
};

const fetchUser = async (): Promise<UserType> => {
  const response = await fetch('/api/user');
  return response.json();
};

// Usage
const App: React.FC = () => {
  const [user, setUser] = React.useState<UserType | null>(null);

  React.useEffect(() => {
    // This is a mock example, not the exact code.
    fetchUser().then(setUser);
  }, []);

  return user ? <UserCard user={user} /> : <div>Loading...</div>;
};


Enter fullscreen mode Exit fullscreen mode

2. Open/Closed Principle (OCP)

Components should be open for extension but closed for modification.

Real-life Example

💡 You can add new apps to your phone without changing the phone's core functionality.

Code Example

// Bad Example (Violating OCP)
type NotificationType = 'email' | 'message' | 'alert';

interface NotificationBadgeProps {
  type: NotificationType;
  count: number;
}

const NotificationBadge: React.FC<NotificationBadgeProps> = ({ type, count }) => {
  if (type === 'email') {
    return <div className="email-badge">{count} Emails</div>;
  } else if (type === 'message') {
    return <div className="message-badge">{count} Messages</div>;
  } else if (type === 'alert') {
    return <div className="alert-badge">{count} Alerts</div>;
  }
  return null;
};

// Good Example (Following OCP)
interface BadgeConfig {
  className: string;
  label: string;
}

const badgeStyles: Record<string, BadgeConfig> = {
  email: { 
    className: 'email-badge',
    label: 'Emails'
  },
  message: {
    className: 'message-badge',
    label: 'Messages'
  },
  alert: {
    className: 'alert-badge',
    label: 'Alerts'
  }
};

interface ExtendedNotificationBadgeProps {
  type: string;
  count: number;
}

const NotificationBadge: React.FC<ExtendedNotificationBadgeProps> = ({ type, count }) => {
  const config = badgeStyles[type];

  if (!config) return null;

  return (
    <div className={config.className}>
      {count} {config.label}
    </div>
  );
};

// Here is an example of extending without modifying existing code
const extendedBadgeStyles = {
  ...badgeStyles,
  social: {
    className: 'social-badge',
    label: 'Social'
  }
};
Enter fullscreen mode Exit fullscreen mode

3. Liskov Substitution Principle (LSP)

Subcomponents should work seamlessly when substituted for their parent.

Note: Liskov substitution was introduced by Barbara Liskov in 1987.

Real-life Example

💡 Imagine a toy car. If someone swaps it for a remote-controlled car, it should still work as expected when you push it.

Code Example

// Bad Example (Violating LSP)
const Input: React.FC<{ type: 'text' | 'number' }> = ({ type }) => {
  if (type === 'number') {
    return <input type="number" />;
  }
  return <input type="text" />;
};


// Good Example (Following LSP)
const TextInput: React.FC = () => <input type="text" />;
const NumberInput: React.FC = () => <input type="number" />;

const FormField: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => (
  <div>
    <label>{label}</label>
    // you will be able to send any children.
    {children}
  </div>
);

// Usage
const App: React.FC = () => (
  <FormField label="Age">
    <NumberInput />
  </FormField>
);

Enter fullscreen mode Exit fullscreen mode

4. Interface Segregation Principle (ISP)

Don’t force components to implement props or methods they don’t use.

Real-life Example

💡 Think of a menu in a restaurant. If you’re ordering pizza, you shouldn’t have to look at sushi options unless you want to.

Code Example

Split large interfaces into smaller, focused ones.


// Types for User-related operations
interface User {
  id: number;
  name: string;
  email: string;
}

type FetchState<T> = {
  data: T | null;
  loading: boolean;
  error: Error | null;
};

// Bad Example (Violating ISP)
 interface UserProps {
  name: string;
  age: number;
  onClick: () => void;
}

const UserCard: React.FC<UserProps> = ({ name, age, onClick }) => (
  <div onClick={onClick}>
    {name} is {age} years old.
  </div>
);


// Good Example (Following ISP) first.
 interface UserInfoProps {
  name: string;
  age: number;
}

interface ClickableProps {
  onClick: () => void;
}

const UserInfo: React.FC<UserInfoProps> = ({ name, age }) => (
  <div>{name} is {age} years old.</div>
);

const Clickable: React.FC<ClickableProps & { children: React.ReactNode }> = ({ onClick, children }) => (
  <div onClick={onClick}>{children}</div>
);

// Usage
const App: React.FC = () => (
  <Clickable onClick={() => alert('Clicked!')}>
    <UserInfo name="John" age={30} />
  </Clickable>
);




Enter fullscreen mode Exit fullscreen mode

Or

interface UserProps {
  name: string;
  age: number;
 // so no more required to add Click handling.
 onClick?: () => void;
}
const UserCard: React.FC<UserProps> = ({ name, age, onClick }) => (
  <div onClick={onClick}>
    {name} is {age} years old.
  </div>
);
Enter fullscreen mode Exit fullscreen mode

5. Dependency Inversion Principle (DIP)

High-level components should depend on abstractions, not details.

Real-life Example

💡 Think of a lamp. The switch shouldn’t care if the lamp is LED or fluorescent. It just needs to know it’s a light source.

Code Example 1


Enter fullscreen mode Exit fullscreen mode

and also you can use hooks or context to abstract dependencies.

Example

//Bad Example of (DIP)
const ThemeButton: React.FC = () => {
  const theme = 'dark';
  return <button className={theme === 'dark' ? 'btn-dark' : 'btn-light'}>Click me</button>;
};

// Good Example of (DIP)
const useTheme = () => React.useContext(ThemeContext);

const ThemeButton: React.FC = () => {
  const theme = useTheme();
  return <button className={theme === 'dark' ? 'btn-dark' : 'btn-light'}>Click me</button>;
};

// Context provider
const ThemeContext = React.createContext('light');
const App: React.FC = () => (
  <ThemeContext.Provider value="dark">
    <ThemeButton />
  </ThemeContext.Provider>
);
Enter fullscreen mode Exit fullscreen mode

Conclusion

Applying SOLID principles in React with TypeScript helps create more:

  • Type-safe components
  • Maintainable and predictable code
  • Easily testable implementations
  • Flexible and extensible applications

Pro Tip: Use TypeScript's type system to enforce these principles. Interfaces and generics can help create more robust and flexible code structures.

Remember, these principles are guidelines. Apply them thoughtfully, balancing between over-engineering and keeping your code clean and understandable.

Thanks For Reading.

Top comments (0)