Frontend Masters: SOLID Principles in React / React Native
The SOLID principles, originally formulated for object-oriented programming, have transcended their origins to become fundamental guidelines for writing maintainable, scalable code across paradigms. While React and React Native embrace functional programming patterns with hooks and components, these time-tested principles remain remarkably relevant. Understanding how to apply SOLID principles in modern React development can transform your codebase from a tangled mess into an elegant, maintainable architecture.
1. Understanding SOLID: More Than Just OOP
SOLID is an acronym representing five design principles introduced by Robert C. Martin (Uncle Bob) that promote software design practices leading to more understandable, flexible, and maintainable code. These principles are the Single Responsibility Principle, Open/Closed Principle, Liskov Substitution Principle, Interface Segregation Principle, and Dependency Inversion Principle.
While originally conceived for object-oriented systems, the core ideas translate beautifully to React’s component-based architecture. The key is understanding the spirit of each principle rather than applying them mechanically. In React, components serve as our primary unit of composition, and how we structure these components determines our application’s long-term maintainability.
Learn more: Robert C. Martin’s Clean Code Principles
2. Single Responsibility Principle: One Component, One Job
The Principle Defined
The Single Responsibility Principle states that a module should have one, and only one, reason to change. In React terms, this means each component should do one thing well. When a component has multiple responsibilities, changes to one responsibility can inadvertently break another, creating a maintenance nightmare.
The Problem: Kitchen Sink Components
Consider a user profile component that fetches data, handles form validation, manages authentication state, formats dates, and renders the UI. This component has at least five distinct responsibilities, making it difficult to test, reuse, or modify without risk.
The Solution: Focused Components
Break down complex components into smaller, focused units. A well-designed profile feature might include a ProfileContainer that handles data fetching, a ProfileForm managing form logic, a ProfileDisplay for rendering, and utility hooks like useAuth and useFormatting for cross-cutting concerns.
Each piece has a clear purpose. The form component doesn’t know about data fetching. The display component doesn’t handle authentication. When you need to change date formatting, you modify one place without touching authentication or form validation.
Practical Example Structure
Your user profile might decompose into UserProfileContainer for orchestration and data management, UserProfileHeader displaying avatar and basic info, UserProfileForm handling edits, UserProfileStats showing activity metrics, and useUserData hook for data fetching logic.
This decomposition makes each piece independently testable and reusable. Need user stats elsewhere? Import UserProfileStats. Want to test form validation? Test UserProfileForm in isolation without worrying about API calls.
3. Open/Closed Principle: Extension Without Modification
The Principle Defined
Software entities should be open for extension but closed for modification. In React, this means designing components that can accommodate new features without changing their existing code. This principle is crucial for maintaining stability as applications grow.
The Problem: Rigid Components
Imagine a button component with hardcoded styles for primary, secondary, and danger variants using if-else statements. Every time you need a new variant—warning, success, outline—you modify the component’s internals, risking bugs in existing variants.
The Solution: Composition and Configuration
Use props, composition, and render props to make components extensible. A well-designed button accepts a variant prop and maps it to styles defined externally. Better yet, it accepts custom styling through a className or style prop, allowing unlimited extension without touching the component.
React’s composition model naturally supports this principle. Instead of building monolithic components that handle every case internally, build small, composable pieces that can be combined in different ways.
Real-World Application: Modal Components
A modal component following Open/Closed might accept children for content, header and footer slots for customization, and size variants through props. Rather than building every possible modal variation into one component, you create a flexible container that consumers extend for their specific needs.
When you need a confirmation modal, you compose the base modal with confirmation-specific content. Need a form modal? Same base component, different children. The modal component never changes, yet it handles infinite use cases through composition.
Learn more: React Composition Patterns
4. Liskov Substitution Principle: Predictable Substitutability
The Principle Defined
Objects of a superclass should be replaceable with objects of its subclasses without breaking the application. In React’s functional world, this translates to: components should be substitutable with their variations without surprising behavior changes.
The Problem: Unpredictable Variations
Consider a Button component and a SubmitButton variant. If SubmitButton ignores the onClick prop that Button accepts, or if it requires additional props that Button doesn’t, consumers can’t safely substitute one for the other. Code expecting a Button breaks when given a SubmitButton.
The Solution: Consistent Interfaces
Ensure component variations maintain compatible interfaces with their base forms. If Button accepts onClick, disabled, and children, then IconButton, SubmitButton, and LinkButton should all accept these props with consistent behavior.
This doesn’t mean variants can’t add their own props—IconButton might add an icon prop—but they shouldn’t break existing contracts. A consumer using the base props should see predictable behavior regardless of which variant they use.
Practical Pattern: Input Components
Text inputs, number inputs, date inputs, and select dropdowns should all share a common interface: value, onChange, disabled, placeholder, error. Form components can then work with any input type without special cases. Swapping a text input for a date picker doesn’t require changing form logic.
This principle shines in form libraries and design systems where components must be interchangeable. A form shouldn’t care whether it’s rendering a text input or a custom masked input—both implement the same interface.
5. Interface Segregation Principle: Minimal Dependencies
The Principle Defined
No client should be forced to depend on methods it doesn’t use. In React terms, don’t burden components with props they don’t need. Fat, bloated prop interfaces make components harder to use and understand.
The Problem: Prop Explosion
A table component that requires 20 props—sorting functions, filtering callbacks, pagination handlers, custom renderers, styling overrides—becomes unwieldy. Most consumers only need a few props, but they’re forced to understand and potentially provide all of them.
The Solution: Focused Interfaces and Composition
Break complex components into smaller pieces with focused prop interfaces. Instead of one massive table component, create Table, TableHeader, TableBody, TableRow, and TableCell components. Consumers compose these building blocks, only using the pieces they need.
For features like sorting and pagination, use composition or separate hook patterns. A useTableSort hook manages sorting logic independently. Components opt into features rather than receiving everything by default.
Optional Props and Sensible Defaults
When components do need multiple props, make most of them optional with sensible defaults. A button component might accept 15 props for complete customization, but only require children. The other 14 have defaults that work for 80% of use cases.
Document which props typically go together. If enabling pagination requires both currentPage and onPageChange, group these in documentation and consider an alternative API like a paginationConfig object.
6. Dependency Inversion Principle: Depend on Abstractions
The Principle Defined
High-level modules should not depend on low-level modules; both should depend on abstractions. In React, this means components should depend on interfaces or contracts, not concrete implementations. This principle is crucial for testability and flexibility.
The Problem: Tight Coupling
A component that directly imports and instantiates an API client creates tight coupling. The component can’t be tested without the real API. It can’t easily switch to a different data source. The implementation detail (how data is fetched) is baked into the high-level component (what data to display).
The Solution: Dependency Injection and Abstractions
Pass dependencies as props or use context. A dashboard component shouldn’t import fetchUserData directly. Instead, it accepts a userDataProvider prop or consumes a UserDataContext. Tests inject a mock provider. Different environments inject different implementations.
React’s context system and hooks provide natural dependency injection mechanisms. A useApi hook can abstract API details, returning just the interface components need: loading state, data, and error. The actual HTTP client is an implementation detail consumers never see.
Practical Pattern: Data Fetching
Instead of components making direct API calls, create custom hooks that abstract data operations. useUserProfile, useOrderList, and useAnalytics hide implementation details. Components depend on the hook interface, not the underlying fetch implementation.
This abstraction allows swapping REST APIs for GraphQL, adding caching layers, or implementing offline-first strategies without changing components. Tests mock hooks instead of complex API setups.
Learn more: Kent C. Dodds on Dependency Injection in React
7. SOLID Principles in Action: A Comparison
| Principle | Without SOLID | With SOLID | Benefit |
|---|---|---|---|
| Single Responsibility | 500-line component handling everything | Multiple 50-100 line focused components | Easier testing, better reusability |
| Open/Closed | Modify component code for new variants | Extend through props and composition | Stable, unchanging base code |
| Liskov Substitution | Incompatible component variations | Consistent interfaces across variants | Safe substitution, predictable behavior |
| Interface Segregation | 20 required props, complex to use | Focused props, sensible defaults | Easier to understand and use |
| Dependency Inversion | Direct API imports in components | Injected dependencies via props/context | Testable, flexible architecture |
8. Visual Guide
9. Real-World Application: Building a Feature
Let’s examine how SOLID principles guide building an article list feature for a content platform. Without SOLID principles, you might build a single ArticleList component that fetches articles from the API, filters them based on user preferences, handles pagination, manages loading states, and renders everything including individual article cards with interaction handlers.
This monolithic component becomes difficult to test—you need to mock the API, set up user preferences, and test all interactions together. It’s hard to reuse—want article cards elsewhere? You’d need to copy code or extract them later. Changes are risky—modifying pagination might break filtering or rendering.
SOLID-Driven Architecture
Following SOLID principles, you’d structure the feature differently. The Single Responsibility Principle guides you to separate concerns into ArticleListContainer for data orchestration, ArticleFilters for filter UI and logic, ArticlePagination for page controls, ArticleGrid for layout, ArticleCard for individual item display, and useArticles hook for data fetching.
The Open/Closed Principle ensures your ArticleCard accepts customization through props without modification. Need featured article styling? Pass a featured prop. Want custom click handling? Pass an onArticleClick callback. The base component stays closed to modification.
Liskov Substitution means your ArticleCard, FeaturedArticleCard, and CompactArticleCard all implement the same core interface. Anywhere accepting an ArticleCard can receive any variant without breaking.
Interface Segregation keeps prop interfaces minimal. ArticleCard only requires article data and optional handlers—it doesn’t know about filters, pagination, or loading states. Consumers aren’t forced to provide irrelevant props.
Dependency Inversion abstracts data fetching. Components don’t import API clients directly. They use useArticles(filters, pagination), which abstracts whether articles come from REST, GraphQL, or local storage. Tests inject mock implementations.
10. Common Pitfalls and Anti-Patterns
Over-Engineering Simple Components
Not everything needs maximum flexibility. A simple label component doesn’t need dependency injection. Apply SOLID principles where complexity justifies the structure, not everywhere reflexively. A component that will never change doesn’t need to be open for extension.
Balance is crucial. Small utility components can be simple and focused without elaborate abstraction layers. Save architectural patterns for components that genuinely benefit from them—those that are complex, frequently modified, or widely reused.
Premature Abstraction
Don’t abstract before understanding actual needs. Wait until you have two or three concrete use cases before creating abstractions. Premature abstraction creates complexity without benefit, making code harder to understand for no gain.
Follow the rule of three: when you’ve written similar code three times, consider abstracting. Before that, duplication might actually be clearer than a premature abstraction that doesn’t quite fit any use case.
Ignoring React’s Nature
React is functional and declarative. Don’t force object-oriented patterns that fight React’s grain. Class inheritance hierarchies, for instance, are an anti-pattern in React. Use composition, not inheritance.
SOLID principles adapt to React’s paradigm. Think in terms of hooks, components, and composition rather than classes and inheritance. The principles’ spirit matters more than their original form.
11. Testing with SOLID Principles
Components following SOLID principles are inherently more testable. Single responsibility means unit tests focus on one thing. Dependency inversion allows mocking external dependencies. Interface segregation makes test setup simpler with fewer required props.
A well-structured component tests easily in isolation. Mock its dependencies, provide minimal required props, and verify its single responsibility. Integration tests become simpler too—components with clear interfaces compose predictably.
Learn more: React Testing Library Best Practices
12. React Native Considerations
SOLID principles apply equally to React Native, but platform-specific concerns add nuances. Performance is often more critical on mobile devices, so Single Responsibility includes considering render performance—components should also have one performance characteristic.
Platform differences require abstraction. A component rendering differently on iOS versus Android benefits from Dependency Inversion—platform-specific implementations hide behind a common interface. Open/Closed principle helps here too—extend platform behavior without modifying shared code.
Native module integration particularly benefits from Dependency Inversion. Abstract native functionality behind JavaScript interfaces that components consume. This makes web sharing easier and testing simpler.
Learn more: React Native Official Documentation
13. Refactoring Existing Code
When inheriting legacy code violating SOLID principles, refactor incrementally. Don’t attempt a complete rewrite. Identify the component with the most violations or causing the most pain, apply SOLID principles there, and move to the next.
Start with Single Responsibility—it often unlocks other improvements. Break apart monolithic components. Extract hooks for data management. Separate presentation from logic. Each step makes the next easier.
Document your refactoring patterns for the team. When you successfully apply Open/Closed to create an extensible component, share the approach. Build organizational knowledge around these principles.
14. Advanced Patterns and Techniques
Compound Components
Compound components naturally implement Open/Closed and Interface Segregation. A Select component with Select.Trigger, Select.Menu, and Select.Item subcomponents provides flexibility through composition while maintaining focused interfaces.
Render Props and Slots
These patterns support Open/Closed by allowing consumers to inject custom behavior without modifying components. A data table accepting renderRow or renderEmpty props extends functionality while keeping core logic closed.
Custom Hooks for Logic Reuse
Hooks are React’s answer to Dependency Inversion and Single Responsibility. Extract stateful logic into custom hooks that provide interfaces to components. useFormValidation, useDebounce, and useLocalStorage are abstractions components depend on.
Learn more: Patterns.dev – React Patterns
15. Building a SOLID Design System
Design systems particularly benefit from SOLID principles. Components must work together cohesively while remaining independently maintainable. Single Responsibility ensures each component has a clear purpose in the system.
Open/Closed makes the design system extensible for specific applications without forking code. Liskov Substitution ensures consistent behavior across variants. Interface Segregation keeps components simple to use. Dependency Inversion allows theming and configuration flexibility.
A SOLID-based design system becomes a genuine productivity multiplier rather than a maintenance burden. Teams can confidently build on it knowing components behave predictably and extend gracefully.
Learn more: Material-UI Design System
16. Measuring Success
How do you know if SOLID principles are working? Look for these indicators: reduced bugs in existing components after adding features, faster implementation of new features using existing components, easier onboarding for new developers, higher test coverage with less effort, and fewer merge conflicts due to better separation of concerns.
Code reviews become smoother when everyone follows SOLID principles. Pull requests are easier to understand because components do one thing. Changes are localized because responsibilities are separated. The codebase feels coherent rather than chaotic.
17. What We’ve Learned
The SOLID principles, while originating in object-oriented programming, translate remarkably well to React and React Native development. By applying Single Responsibility, we create focused components that are easy to understand and test. Open/Closed principle guides us toward extensible architectures using composition and configuration. Liskov Substitution ensures our component variations behave predictably and substitutably. Interface Segregation keeps component APIs minimal and focused. Dependency Inversion abstracts implementation details, making code flexible and testable.
These principles aren’t rigid rules but guiding philosophies that help us make better design decisions. The key is understanding their spirit and adapting them to React’s functional, component-based paradigm. A component-based architecture naturally aligns with many SOLID concepts, but conscious application of these principles elevates code quality significantly.
Success with SOLID principles requires balance. Over-engineering simple components wastes effort, while ignoring these principles in complex features creates technical debt. Start with components that genuinely benefit from better structure—those that are complex, frequently modified, or widely reused. As you gain experience applying SOLID principles in React, you’ll develop intuition for when and how to apply each principle effectively. The result is a codebase that scales gracefully, welcomes new features without breaking existing ones, and remains maintainable as your application and team grow.







