CLEAN CODE IN REACT: EMBRACE SOLID PRINCIPLES FOR ENHANCED DEVELOPMENT​

 

React with its declarative and component-based architecture, has revolutionized front-end development. To maximize the benefits of this popular JavaScript library, it’s crucial to adopt clean coding practices. Clean code in React not only enhances the readability and maintainability of your applications but also facilitates collaboration among developers. In this blog post, we’ll explore key clean coding practices specific to React development.

1. Single Responsibility Principle (SRP)

Each React component should have a single responsibility. Dividing complex components into smaller, focused ones, making it easier to understand, test, and maintain. That means each component should have only one reason to change. For example:

We will start with a very simple and basic example, Imagine we have a component called [UserProfile] that displays username and email as well as his hobbies, here to have SRP we can create separate HOCs (Higher Order Component).

import React, { useState } from 'react';

function UserProfile({ user }) {

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <h2>Hobbies</h2>
      <ul>
        {user.hobbies.map((hobby, index) => (
          <li key={index}>{hobby}</li>
        ))}
      </ul>
    </div>
  );
}

First component will be [UserInfo]:

const UserInfo = ({user}) => {

  return (
    <>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </>
  );
}

Second component will be [UserHobbies]:

const UserHobbies = ({ hobbies }) => (
  <div>
    <h2>Hobbies</h2>
    <ul>
      {hobbies.map((hobby, index) => <li key={index}>{hobby}</li>)}
    </ul>
  </div>
);

Updated [UserProfile] Component with SRP:

function UserProfile({ user }) {
  return (
    <div>
      <UserInfo user={user} />
      <UserHobbies hobbies={user.hobbies} />
    </div>
  );
}

By applying the Single Responsibility Principle, we can refactor a complex React component into smaller, more focused components. This approach enhances the readability, maintainability, and scalability of your React applications. This might look like a really basic and simple example, but it will make sense if you’re working on bigger projects, it’s better to follow this principle instead of stacking large amount of code in one place.

2. Open/Closed Principle (OCP)

This means you should be able to add new features without changing your existing code. An Example can be a Button or Notification Component which you can create once and re-use in multiple other components. For better understanding, let’s check the following example:

First, let’s create a base notification component that can be used to support different types of notifications without modifying its code.

<span style="font-size: 15px;">const Notification = ({ text, className }) => {
  return <div className={`notification ${className}`}>{text}</div>;
};</span>

This component takes as Props two elements text and className, these two will change based on what you send using them.
Here is an Example on how we can use and re-use this component without modifying the original code, adhering to the OCP:

// SuccessNotification.js
const SuccessNotification = ({ text}) => {
  return <Notification className="success">{text}</Notification>;
};

// ErrorNotification.js
const ErrorNotification = ({ text}) => {
  return <Notification className="error">{text}</Notification>;
};

// WarningNotification.js
const WarningNotification = ({ children }) => {
  return <Notification className="warning">{text}</Notification>;
};

3. Liskov Substitution Principle (LSP)

Liskov Substitution Principle (LSP), a principle that can make our React applications more flexible and maintainable. It basically means if you have used a component A and you replaced it with component B, it should behave just like the base component. For example, replacing ButtonComponent with IconButtonComponent.
Here is a more detailed example:

Imagine you have a simple Button component in your React application:

function Button({ onClick, children }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
}

This Button takes two props: onClick (what happens when you click the button) and children (the text or elements inside the button).

Now, let’s say you want a button that not only clicks but also shows a loading indicator when something is happening. You’ll create a LoadingButton component like this:

function LoadingButton({ onClick, children, loading }) {
  return (
    <button onClick={onClick} disabled={loading}>
      {loading ? 'Loading...' : children}
    </button>
  );
}

This new component behaves just like the button component but it loads to show the user that something is happening.
So, according to LSP you should be able to use LoadingComponent anywhere without having to change your original Button code.

4. Interface Segregation Principle (ISP)

A short definition for ISP would be, components should not depend on props and methods that it doesnt need.

What does this mean? Simply, it means we should not use any interface if we don’t need all of its capabilities. To keep our code focused and relevant, we should divide interfaces that are overloaded with too many methods or properties. This approach also aligns with the open-closed and single-responsibility principles. Given that interfaces as understood in traditional programming languages are not directly applicable in React, we approximate interface behavior through the use of simple, focused components, similar to the examples provided earlier.

We have a user profile component that could potentially offer several functionalities: displaying user details, editing user information, and sending a message to the user. Applying ISP, we would avoid creating one large component with all these capabilities.

function UserProfile({ userDetails }) {
  return (
    <div>
      <h1>{userDetails.name}</h1>
      <p>{userDetails.bio}</p>
    </div>
  );
}

 

We have a UserProfile component which only need few data from the userDetails props which are name and bio, but the userDetails props have too many data {created_at, updated_at, email, lastname}. By giving UserProfile userDetails as a props, we end up giving it more than the component actually need.
To fix this issue, we only send the props that UserProfile actually needs.

function UserProfile({ name, bio}) {
  return (
    <div>
      <h1>{name}</h1>
      <p>{bio}</p>
    </div>
  );
}

 

5. Dependency Inversion Principle (DIP)

A short definition for DIP would be, we should avoid write dependencies on a specific component, instead it should be able to accept callback functions. For example, instead of calling an api to fetch data in the same component, create a hook or a new file that will export that functionality then use it in your component, that makes your code much more cleaner.

import React, { useEffect, useState } from 'react';

function BlogPosts() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    fetch('https://example.com/api/posts')
      .then(response => response.json())
      .then(data => setPosts(data))
      .catch(error => console.error('Error fetching posts:', error));
  }, []);

  return (
    <div>
      {posts.map(post => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  );
}

 

In the above example, the BlogPosts function is directly dependent on the low-level module (fetch API call to https://example.com/api/posts). The main responsibility of this component is to display the data, it shouldn’t care about fetching it.

To apply DIP, we move the data fetching logic into a separate module and depend on this function instead.
First, define a function for fetching posts. This could be an interface in TypeScript or JavaScript.

// you can name it postsService.js
export const fetchPosts = () => {
  return fetch('https://example.com/api/posts')
    .then(response => response.json())
    .catch(error => console.error('Error fetching posts:', error));
};

 

Next, modify BlogPost component to use that function.

import React, { useEffect, useState } from 'react';

function BlogPosts({ fetchPosts }) {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    fetchPosts()
      .then(data => setPosts(data))
      .catch(error => console.error('Error:', error));
  }, [fetchPosts]);

  return (
    <div>
      {posts.map(post => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  );
}

 

Conclusion

Clean coding practices in React are essential for building maintainable, scalable, and bug-free applications. By following principles like SRP, organizing components thoughtfully, managing state and props effectively, writing clean JSX, maintaining code readability, and incorporating testing and documentation, developers can create React applications that are not only functional but also a joy to work with. Clean code is an investment in the future of your project, enabling smoother collaboration and easier maintenance as your application evolves.