Sitemap
Silversky Technology

Silversky Technology

Transforming Visionary Ideas into Cutting-Edge Solutions

SOLID Principles in React Native: Build Scalable, Maintainable, and Testable Applications

8 min read13 hours ago

--

Press enter or click to view image in full size

As React Native applications grow, maintaining clean, reusable, and scalable code becomes increasingly challenging. Many projects start with a few screens and simple components but eventually evolve into complex applications containing hundreds of screens, APIs, state management, navigation flows, business logic, and platform-specific implementations.

Without a proper architecture, developers often encounter:

  • Massive components
  • Duplicate business logic
  • Tight coupling
  • Difficult testing
  • Frequent regressions
  • Slow development

This is where SOLID principles help. By following SOLID, your React Native code becomes easier to maintain, easier to test, easier to extend, more reusable, and cleaner to read.

What Is SOLID?

SOLID is an acronym for five software design principles:

Letter Principle S Single Responsibility Principle O Open/Closed Principle L Liskov Substitution Principle I Interface Segregation Principle D Dependency Inversion Principle

Let’s understand each principle with React Native examples.

1. Single Responsibility Principle (SRP)

Definition: A class, component, hook, or module should have only one reason to change. In React Native, every component should perform one responsibility.

❌ Existing Approach

function ProfileScreen() {
const [user, setUser] = useState(null);

useEffect(() => {
axios.get('/profile').then(res => setUser(res.data));
}, []);
const logout = async () => {
await AsyncStorage.clear();
};
return (
<View>
<Text>{user?.name}</Text>
<Button title="Logout" onPress={logout} />
</View>
);
}

This violates SRP: the component fetches data, manages auth state, and renders UI all at once.

✅ Improved Approach

Split responsibilities:

// auth.ts
export const logout = () => AsyncStorage.clear();

// useProfile.ts
export function useProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
axios.get('/profile').then(res => setUser(res.data));
}, []);
return user;
}
// ProfileScreen.tsx
function ProfileScreen() {
const user = useProfile();
return (
<View>
<Text>{user?.name}</Text>
<Button title="Logout" onPress={logout} />
</View>
);
}

Now each module has only one responsibility.

Benefits: easier testing, easier debugging, better readability, better reusability.

2. Open/Closed Principle (OCP)

Definition: Software entities should be open for extension but closed for modification. Instead of modifying existing code every time requirements change, extend it.

❌ Bad Example

function Button({ type }: { type: 'primary' | 'secondary' }) {
if (type === 'primary') {
return <Pressable style={styles.primary} />;
}
if (type === 'secondary') {
return <Pressable style={styles.secondary} />;
}
}

Adding a new button type means editing the component’s internal branching logic.

✅ Better Example

Move the variant-to-style mapping outside the component, so a new variant is an addition to the map, not a change to the component:

type Variant = 'primary' | 'secondary' | 'danger';

const variantStyles: Record<Variant, ViewStyle> = {
primary: styles.primary,
secondary: styles.secondary,
danger: styles.danger,
};
function Button({ variant = 'primary', ...props }: { variant?: Variant } & PressableProps) {
return <Pressable style={variantStyles[variant]} {...props} />;
}

Adding warning later means adding one line to variantStyles and one value to Variant — Button itself never changes. The same idea applies at a larger scale with a ThemeProvider: instead of branching on if (theme === 'dark') throughout your codebase, components read theme values from context and the theme definitions are what get extended.

3. Liskov Substitution Principle (LSP)

Definition: Objects of a derived type should be replaceable with objects of their base type without changing the correctness of the application. In simple terms: if one component replaces another, the rest of your application shouldn’t need to know or change. The new component should support the same expected behavior and contract.

A Simple Real-Life Analogy

Imagine every charging cable in your house uses a USB-C connector. Your phone charger works with it, your tablet charger works with it, your laptop charger works with it. You can swap one for another because they all follow the same interface. Now imagine someone gives you a charger that looks like USB-C but doesn’t fit your phone. Even though it appears similar, it can’t replace the original. That’s a violation of the Liskov Substitution Principle.

Understanding LSP in React Native

React Native doesn’t use inheritance very often. Instead, we build applications using components, props, composition, and custom hooks. In React Native, LSP means that if two components are intended to be interchangeable, they should accept the same required props and behave consistently.

❌ Before (Violates LSP)

type ButtonProps = {
title: string;
onPress: () => void;
};
function Button({ title, onPress }: ButtonProps) {
return (
<Pressable onPress={onPress}>
<Text>{title}</Text>
</Pressable>
);
}

Used throughout the app:

<Button title="Save" onPress={saveData} />

A new requirement comes in: buttons with icons. A developer creates:

function IconButton({
title,
icon,
}: {
title: string;
icon: React.ReactNode;
}) {
return (
<Pressable>
{icon}
<Text>{title}</Text>
</Pressable>
);
}

Now someone tries to swap Button for IconButton:

<IconButton title="Save" icon={<SaveIcon />} onPress={saveData} />

This throws a type error, because IconButton doesn't even accept onPress. The component changed the expected contract — instead of being a drop-in replacement for Button, it forces callers to change how they use it. This violates LSP.

✅ After (Follows LSP)

Both components expose the same required API:

type ButtonProps = {
title: string;
onPress: () => void;
};

// Button
function Button({ title, onPress }: ButtonProps) {
return (
<Pressable onPress={onPress}>
<Text>{title}</Text>
</Pressable>
);
}
// IconButton
function IconButton({
title,
icon,
onPress,
}: ButtonProps & { icon: React.ReactNode }) {
return (
<Pressable onPress={onPress}>
{icon}
<Text>{title}</Text>
</Pressable>
);
}

Now both can be used interchangeably:

<Button title="Save" onPress={saveData} />
<IconButton title="Save" icon={<SaveIcon />} onPress={saveData} />

The rest of your application doesn’t need to change. This follows the Liskov Substitution Principle.

4. Interface Segregation Principle (ISP)

Definition: Clients should not be forced to depend on interfaces they do not use. In simple terms: don’t make a component receive props, methods, or dependencies it doesn’t need. Instead of one large interface that tries to do everything, split it into smaller, focused interfaces.

A Simple Real-Life Analogy

Imagine a TV remote with only the buttons you actually use: power, volume, channel, mute. Now imagine it also has buttons for the air conditioner, washing machine, microwave, and ceiling fan. Most people would never use those, and the remote becomes confusing. Instead, each device should have its own remote. That’s exactly what ISP encourages.

Understanding ISP in React Native

In React Native, interfaces usually mean component props, TypeScript interfaces, context values, service interfaces, and hook return values. A common mistake is creating one large interface that every component has to use, so many components end up receiving props they never touch.

❌ Before

Get Silversky Technology’s stories in your inbox

Join Medium for free to get updates from this writer.

One hook does everything:

const {
user,
login,
logout,
updateProfile,
uploadPhoto,
deleteAccount,
} = useUser();

A screen that only needs user still receives every other method.

✅ After

Split responsibilities:

const { user } = useProfile();
const { login, logout } = useAuth();
const { uploadPhoto } = usePhotoUpload();

Now every screen imports only what it needs.

Common ISP Violations: A Huge Props Object

<ProductCard
product={product}
onLike={handleLike}
onShare={handleShare}
onDelete={handleDelete}
onReport={handleReport}
onBookmark={handleBookmark}
/>

Most screens only need one or two of these actions.

Benefits of Following ISP: components receive only the props they need, smaller and more readable interfaces, less boilerplate, easier reuse, lower coupling, easier testing, better separation of concerns.

5. Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions. In simple terms: your components shouldn’t know how something is implemented, only what it does. Instead of depending directly on libraries like Axios, AsyncStorage, Firebase, or the Fetch API, your components should depend on a service or abstraction. If the implementation changes, your components don’t have to.

A Simple Real-Life Analogy

Your phone doesn’t know whether its electricity comes from a wall charger, a power bank, a laptop, or a car charger. It only knows it needs a USB-C connection. The USB-C port is the abstraction — you can change the charger anytime without changing the phone. That’s Dependency Inversion.

Understanding DIP in React Native

One of the most common mistakes is putting API calls directly inside screens.

❌ Before (Violates DIP)

function ProfileScreen() {
const [user, setUser] = useState(null);
useEffect(() => {
axios.get('/profile').then(res => setUser(res.data));
}, []);
return <Text>{user?.name}</Text>;
}

This looks fine, but the component directly depends on Axios. If your team later switches to the Fetch API, React Query, RTK Query, or Apollo GraphQL, every screen that calls Axios needs updating.

Imagine a project with 80 screens and 200 API calls, every one of them calling axios.get(...), axios.post(...), axios.put(...) directly. Now the company decides to replace Axios with Fetch. You'd need to edit hundreds of files. That's tight coupling.

✅ After (Follows DIP)

Create a service that hides the implementation:

// profileService.ts
export const profileService = {
getProfile() {
return axios.get('/profile');
},
};

Now the screen doesn’t know whether the service uses Axios, Fetch, or anything else:

function ProfileScreen() {
const [user, setUser] = useState(null);

useEffect(() => {
profileService.getProfile().then(res => setUser(res.data));
}, []);
return <Text>{user?.name}</Text>;
}

When the team decides to remove Axios, only the service changes:

export const profileService = {
async getProfile() {
const response = await fetch('/profile');
return response.json();
},
};

ProfileScreen doesn't change at all. That's Dependency Inversion.

Applying SOLID in React Native Architecture

A scalable folder structure often reflects SOLID principles:

src/
├── components/
├── screens/
├── hooks/
├── services/
├── repositories/
├── usecases/
├── navigation/
├── context/
├── utils/
├── constants/
├── api/
└── assets/

Responsibilities stay separated: screens handle UI composition, components are reusable UI, hooks manage state and side effects, services handle API communication, repositories provide data abstraction, use cases hold business logic, and context carries shared application state.

Real-World Example: Login Flow

Without SOLID, everything lives in one file: the screen handles validation, the API call, storage, navigation, analytics, and the toast notification, all in sequence, all in the same component.

With SOLID, each layer has a single responsibility:

LoginScreen → useLogin hook → LoginUseCase → AuthRepository → Axios API

Each layer hands off to the next, which makes the application easier to maintain and test.

Benefits of SOLID in React Native

Common Mistakes

  1. Putting API calls directly in screens. Bad: LoginScreen → axios.post(). Good: LoginScreen → useLogin → Repository.
  2. Creating huge contexts. Instead of one global context, split concerns into dedicated contexts such as authentication, theme, notifications, and language.
  3. Components with too many responsibilities. A screen should compose UI, not perform validation, networking, analytics, and persistence all at once.
  4. Copy-pasting business logic. Move repeated calculations, formatting, or validation into reusable hooks, services, or utility modules.
  5. Depending directly on third-party libraries. Wrap libraries such as Axios, Firebase, or AsyncStorage behind abstractions so you can replace or mock them without touching your UI.

Best Practices

  • Keep components focused on presentation whenever possible
  • Move API calls to service or repository layers
  • Encapsulate business rules in custom hooks or use cases
  • Favor composition over inheritance for reusable UI
  • Use TypeScript interfaces to define contracts between layers
  • Keep contexts small and domain-specific
  • Write unit tests for hooks, repositories, and use cases
  • Inject dependencies instead of hardcoding implementations
  • Build reusable UI components with configurable props rather than duplicating code
  • Review large components regularly and refactor them into smaller, focused modules

Conclusion

SOLID principles are not limited to traditional object-oriented languages — they provide valuable guidance for structuring React Native applications as well. By applying these principles to components, hooks, services, repositories, and business logic, you create applications that are easier to understand, extend, and maintain.

You don’t need to implement every principle from day one. Start by giving each component a single responsibility, separate business logic from UI, and introduce abstractions where they add real value. As your application grows, these small architectural decisions will reduce technical debt, simplify testing, and make onboarding new developers much easier.

A well-structured React Native project isn’t just about writing code that works today — it’s about building a foundation that continues to work as your product, team, and codebase evolve.

Which of these is hardest to stick to as a codebase grows — SRP, or keeping dependencies properly inverted? Tell us in the comments below.

Brought to you by Parth from the Silversky Technology crew.
Curious what else we’re building? Explore more at silverskytechnology.com.

--

Silversky Technology
Silversky Technology

Written by Silversky Technology

Transforming Visionary Ideas into Cutting-Edge Solutions