Logo
Published on

Frontend Masters: Solid Principles in React / React Native

Authors
  • Name
    Twitter

If you are a software developer, you may face_ “Have you ever heard of Solid principles before?” questions in interviews. You may already have come across a question like this. I’m sure you’ve also mentioned that you’ve heard about it and used it in daily life, but you can’t exemplify it right now. 😂 Let’s now learn what SOLID is and how can we apply these principles in React/React Native projects with examples so that we’ll never forget it in future interviews.😎

Before we start: This will be a long article, grab your coffee now :) ☕️

SOLID is a group of five important rules in software design. These rules help make the code understandable, flexible, and maintainable.


1. Single Responsibility Principle (SRP)

A class ought to serve a single, clearly defined purpose, reducing the need for frequent changes.

We can stick to the Single Responsibility Principle (SRP) in our React/React Native project by being sure that each component in our application has a specific, well-defined purpose. For example, a component could be responsible for displaying a particular part, handling user input, or making API calls to fetch data. By limiting a component to a single, well-defined task, we may improve the clarity and maintainability of our codebase.

Here are some guidelines for implementing the Single Responsibility Principle (SRP) in a React application:

  1. Make your components small and do one thing: Try to keep your components small and focused, assigning each one a singular, well-defined responsibility.
  2. Don’t mix different jobs: Don’t bundle unrelated tasks into one component. For instance, a component in charge of showing a form should not also handle making API calls to fetch list data.
  3. Use composition: Create reusable UI components by combining smaller components. This allows developers to break down complex UIs into smaller, more manageable parts, which can be easily reused across different parts of the application.
  4. Handle props and state wisely: Props are like messengers that carry data and actions to child components. On the other hand, a state is like a personal note-keeper for a component, holding its unique information. Use state for info that’s not limited to one piece.

To explain this idea with an anti-pattern sample, look at the following basic code snipped. This code’s job is to get and show list items.

import React, { useEffect, useState } from "react";  
import { View, Text, Image } from "react-native";  
import axios from "axios";  
  
  
const ListItems = () => {  
 const [listItems, setListItems] = useState([]);  
 const [isLoading, setIsLoading] = useState(true);  
  
  
 useEffect(() => {  
   axios.get("https://.../listitems/").then((res) => {  
       setListItems(res.data)  
   }).catch(e => {  
       errorToast(e.message);  
   }).finally(() => {  
       setIsLoading(false);  
   })  
 }, [])  
  
  
 if (isLoading){  
   return <Text>Loading...</Text>  
 }  
  
  
 return (  
   <View>  
       {listItems.map(item => {  
           return (  
               <View>  
                    <Image src={{uri:item.img}} />  
                    <Text>{item.name}</Text>  
                    <Text>{item.description}</Text>  
               </View>  
           )  
       })}  
   </View>  
 );  
};  
  
  
export default ListItems;

At first glance, the code may seem well-written, as it aligns with what we’re used to. Yet, as we deep dive into the component definition, we start noticing some “issues.” The ListItems component takes on multiple roles:

  1. Managing the state.
  2. Making requests for list items and handling the fetching process.
  3. Rendering the component.

Though it may look like a typical component initially, according to the Single Responsibility Principle (SRP), it’s handling more responsibilities than it should.

So, how can we make it better?

Typically, if there’s a useEffect inside a component, we can create a custom hook to handle that action and keep the useEffect out of the component.

In this case, we’ll create a new file with a custom hook. This hook will take charge of the logic for fetching list data and managing the state.

import React, { useEffect, useState } from "react";  
import axios from "axios";  
  
  
const useFetchListItems = () => {  
   const [listItems, setListItems] = useState([]);  
   const [isLoading, setIsLoading] = useState(true);  
  
useEffect(() => {  
   axios.get("https://.../listitems/").then((res) => {  
       setListItems(res.data)  
   }).catch(e => {  
       errorToast(e.message);  
   }).finally(() => {  
       setIsLoading(false);  
   })  
 }, [])  
  
   return { listItems, isLoading };  
}

By separating the tasks of handling the state and fetching list items, our initial component becomes significantly simpler and more straightforward to read and understand. Its only responsibility is now to display information, making it more maintainable and extendible.

Our refactored and compatible with SRP component would appear like this:

import { useFetchListItems } from "hooks/useFetchListItems";  
  
  
const ListItems = () => {  
 const { listItems, isLoading } = useFetchListItems();  
  
  
if (isLoading){  
   return <Text>Loading...</Text>  
 }  
  
 return (  
   <View>  
       {listItems.map(item => {  
           return (  
               <View>  
                    <Image src={{uri:item.img}} />  
                    <Text>{item.name}</Text>  
                    <Text>{item.description}</Text>  
               </View>  
           )  
       })}  
   </View>  
 );  
};  
  
  
export default ListItems;

But if we look closely at our new hook, we see it’s doing a couple of things. It handles both managing the state and fetching the list items. So, it doesn’t really follow the idea of doing just one thing, as suggested by the Single Responsibility Principle.

To fix this, we can separate the logic of getting list items. In simple terms, we’ll create a new file called api.js and move the code responsible for fetching list items there:

import axios from "axios";  
import errorToast from "./errorToast";  
  
  
const fetchListItems = () => {  
 return axios  
   .get("https://.../listitems/")  
   .catch((e) => {  
     errorToast(e.message);  
   })  
   .then((res) => res.data);  
};

🎉 And our new refactored custom hook:

import { useEffect, useState } from "react";  
import { fethListItems } from "./api";  
  
  
const useFetchListItems = () => {  
 const [listItems, setListItems] = useState([]);  
 const [isLoading, setIsLoading] = useState(true);  
  
  
 useEffect(() => {  
   fetchListItems()  
     .then((listItems) => setListItems(listItems))  
     .finally(() => setIsLoading(false));  
 }, []);  
  
  
 return { listItems, isLoading };  
};

Let’s talk about keeping things simple. Following the Single Responsibility Principle (SRP) helps us organize our code and avoid mistakes. But, it’s not always easy. It might make our file structure more complicated, and planning can take extra time.

In our example, we made each file do one thing, making our structure a bit more complex but in line with the principle. However, keep in mind that strict adherence to SRP isn’t always the best. Sometimes, it’s okay to have a bit of complexity in our code rather than making it overly complicated.

There are situations where we don’t have to strictly follow SRP, like:

  1. Form components: Forms do many jobs, like checking data, managing state, and updating info. Splitting these tasks can make things messy, especially if we’re using other tools or libraries.
  2. Table components: Tables also handle different tasks, like showing data and managing how users interact. Breaking these into separate pieces might make our code more confusing.

2. Open/Closed Principle (OCP)

‘Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.’*

  • Open for extension: It means you can add new behaviors, functionalities, and features to a component without having to change how it already works.
  • Closed for modification: After you’ve created and implemented a React component, you should avoid directly manipulating its source code unless it is inevitable.

Let’s take a look at the following basic React Native component:

import React from 'react';  
  
interface IListItem {  
  title: string;  
  image: string;  
  isAuth: boolean;  
  onClickMember: () => void;  
  onClickGuest: () => void;  
}  
  
const ListItem: React.FC<ILıstItem> = ({ title, image, isAuth, onClickMember, onClickGuest }: IListItem) => {  
  const handleMember = () => {  
    // Some logic  
    onClickMember();  
  };  
  
  const handleGuest = () => {  
    // Some logic  
    onClickGuest();  
  };  
  return (  
    <View>  
      <Image source={{uri:image}} />  
      <Text>{title}</Text>  
      {  
      isAuth ?   
      <Button onClick={handleMember}>Add to cart +</Button>  
      :  
      <Button onClick={handleGuest}>Show Modal</button>  
      }  
    </View>  
  );  
};

As you see, the code which is above does not fit with the principle. It violets by rendering different features depending on Authentication status.

If we want to render different buttons with different logic it is better we modify this code block:

interface IButtonHandler {  
  handle(): void;  
}  
  
export const GuestButtonHandler: React.FC<{ onClickGuest?: () => void }> = ({ onClickGuest }) => {  
  const handle = () => {  
    // Some logic for guests  
    onClickGuest();  
  };  
  
  return <Button onClick={handle}>Show Modal</Button>;  
};  
  
export const MemberButtonHandler: React.FC<{ onClickMember?: () => void }> = ({ onClickMember }) => {  
  const handle = () => {  
    // Some logic for members  
    onClickMember();  
  };  
  
  return <Button onClick={handle}>Add to cart +</Button>;  
};
import React from 'react';  
  
interface IListItem {  
  title: string;  
  image: string;  
}  
  
export const ListItem: React.FC<IListItem> = ({ title, image, children}) => {  
  
  return (  
    <View>  
      <Image source={{ uri: image }} />  
      <Text>{title}</Text>  
      {children}  
    </View>  
  );  
};  
  
export default ListItem;

And finally, we get rid of the unnecessary codes and create new props which is children so the other component can extend this component by passing it as a child:

import {ListItem} from "../index.tsx"  
import {GuestButtonHandler, MemberButtonHandler} from "../handlers"  
  
const App = () => {  
  return (  
    <ListItem title={item.title} image={item.image}>  
      isAuth ? <MemberButtonHandler /> : <GuestButtonHandler />  
    </ListItem>  
  );  
};  
  
export default App  

🎉 Now our ListItem component will be Open for extension and Closed for modification. This method is more effective because we now have separated components that don’t need lots of props to show various things. We just have to show the right part with the needed features. Also, if a component does lots of things inside, it might be breaking the rule of the Single Responsibility Principle (SRP). So, this new way helps keep the code organized and clear.

3. Liskov Substitution Principle (LSP)

Objects of a superclass should be replaceable with objects of its subclasses.

That means subclasses of a particular class should be able to replace the superclass without breaking any functionality.

Example:

If "RacingCar" is a subclass of "Car", then we should be able to replace instances of "Car" with "RacingCar" without any surprises. That means "RacingCar" should fulfill all the expectations set by the "Car" class.

In React, the Liskov Substitution Principle (LSP) is about being able to easily replace parent components while still doing the same things as a child component. It should continue to work as before if a component uses another component.

Let’s look at the code now:

const SuccessButton = () => {  
  return (  
      <Text>Success</Text>  
  );  
};

So we want to create a **SuccessButton** component but a button functionality couldn't be replaced by a **Text** so this violates the principle. What we should do instead is just return a button like this:

const SuccessButton = () => {  
  return (  
    <TouchableOpacity style={styles.button} onPress={onPress}>  
      <Text>Success</Text>  
    </TouchableOpacity>  
  );  
};

This is better but it is not enough. We also should inherit all the features of the button itself:

const SuccessButton = () => {  
  return (  
    <TouchableOpacity style={styles.button} onPress={onPress} {…props}>  
      <Text>Success</Text>  
    </TouchableOpacity>  
  );  
};

🎉 Now we inherited all of the attributes of the button and we pass the attributes to the new button. Also, any instance of SuccessButton can still be used in place of an instance of Button without changing the program’s behavior and following the Liskov Substitution Principle.

4. Interface Segregation Principle (ISP)

“No code should be forced to depend on methods it does not use”*. For React applications, we will rephrase it as “components should not depend on props they don’t use.”

Let’s dive into an example to understand better:

const ListItem = ({item}) => {  
  
  return (  
    <View>  
      <Image source={{uri:item.image}} />  
      <Text>{item.title}</Text>  
      <Text>{item.description}</Text>  
    </View>  
  );  
};

We have a ListItem component which only needs a few data from theitem props which are image, title, and description. By giving it ListItem as props, we end up giving it more than the component actually needs because theitem props itself might contain data that the component doesn’t need

To solve this problem we may limit the props to only what the component needs.

const ListItem = ({image, title, description}) => {  
  
  return (  
    <View>  
      <Image source={{uri:image}} />  
      <Text>{title}</Text>  
      <Text>{description}</Text>  
    </View>  
  );  
};

🎉 And now our component is compatible with the ISP principle.

5. Dependency Inversion Principle (DIP)

  1. High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).

  2. Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions. *****

In the context of React, this principle ensures high-level components should not directly depend on low-level components, but both should depend on a common abstraction. In this case, “component” refers to any part of the application, whether it’s a React component, a function, a module, a class-based component, or a third-party library. Lets see with examples:

const CreateListItemForm = () => {  
  const handleCreateListItemForm = async (e) => {  
    try {  
      const formData = new FormData(e.currentTarget);  
      await axios.post("https://myapi.com/listItems", formData);  
    } catch (err) {  
      console.error(err.message);  
    }  
  };  
  
  return (  
    <form onSubmit={handleCreateListItemForm}>  
      <input name="title" />  
      <input name="description" />  
      <input name="image" />  
    </form>  
  );  
};

The component above shows a form for handling creating a list item by rendering a form and sending the submitted data to an API.

Consider this scenario. There’s another form for editing a list item with the same UI but differing only in terms of logic(in our example it is an API endpoint). Our form would be unreusable since we need another endpoint to submit our edit form. Therefore, we need to create a component that isn’t dependent on a specific low-level module.

const ListItemForm = ({ onSubmit }) => {  
  return (  
    <form onSubmit={onSubmit}>  
      <input name="title" />  
      <input name="description" />  
      <input name="image" />  
    </form>  
  );  
};

We have removed the dependency from the form and now we can give it the necessary logic through props.

const CreateListItemForm = () => {  
  const handleCreateListItem = async (e) => {  
    try {  
      const formData = new FormData(e.currentTarget);  
      await axios.post("https://myapi.com/listItems", formData);  
    } catch (err) {  
      console.error(err.message);  
    }  
  };  
  return <ListItemForm onSubmit={handleCreateListItem} />;  
};
const EditListItemForm = () => {  
  const handleEditListItem = async (e) => {  
    try {  
      // Editing logic  
    } catch (err) {  
      console.error(err.message);  
    }  
  };  
  return <ListItemForm onSubmit={handleEditListItem} />;  
};

With this simplification and applying DIP, we can even test each component separately without being concerned about unintentionally affecting other connected parts, as there aren’t any.


So far, I have tried to explain all the principles with examples. I hope you learned these principles, which are difficult to remember. I hope this time you don’t even need to memorize them. If you found this article insightful or helpful, consider supporting my work by buying me a coffee. Your contribution helps fuel more content like this. Click here to treat me to a virtual coffee ☕️. Happy hackings! 🚀

BuyMeACoffee

GitHub

LinkedIn