Top

Following S.O.L.I.D – The 5 Object Oriented Principles in React Native Architecture

Without making it sound like a cliche, I would like to start this blog by stating the 3 most important feature of writing code:

  • It should be maintainable
  • It should be extensible and
  • It should have modularity

For the past 6 years, since I am writing code, I have believed that code should be written in a clean and simple way. It should be understood by any other developer in my absence. And I have always tried to find the above three qualities in my code. This search has led me to enforce the coding standard that is appropriate. And guess what I landed upon? The SOLID Principle.

S.O.L.I.D is a software term describing a collection of design principles was invented by Robert C. Martin, also known as Uncle Bob. It is described as:

S — Single responsibility principle

O — Open closed principle

L — Liskov substitution principle

I — Interface segregation principle

D — Dependency Inversion principle

It helps your code to be more extendable, logical and easier to read. My previous blog on React Native Top Ten Best Practices has just shown you that which things have to be taken care during development. But still something I missed to tell you that we can follow S.O.L.I.D if we want.

Let’s see what are the S.O.L.I.D principles in details and how they help building a good quality React Native Architecture.

1. Single Responsibility Principle

This principle says that each module should have one and only one reason to change.

Let’s try to build an application which displays posts in a list item.


class App extends Component {

    state = {
        posts: [{name: 'Top 10 React Native Best Practices', description: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.'}]
    };

    componentDidMount() {
        this.fetchPosts();
    }

    async fetchPosts() {
        const response = await fetch('http://api.innofied.com/posts');
        const posts = await response.json();
        this.setState({posts});
    }

    render() {
        return (
            <div className="App">
                <header className="App-header">
                  // huge amount code for header
                </header>
                <table>
                    <thead>
                        <tr>
                            <th>Name</th>
                            <th>Description</th>
                        </tr>
                    </thead>
                    <tbody>
                        {this.state.posts.map((post, index) => (
                            <tr key={index}>
                                <td><input value={post.name} onChange={/* update name in the state */}/></td>
                                <td><input value={post.description} onChange={/* update description in the state*/}/></td>
                            </tr>
                        ))}
                    </tbody>
                </table>
                <button onClick={() => this.savePostsOnTheBackend()}>Save</button>
            </div>
        );
    }

    savePostsOnTheBackend(row) {
        fetch('http://api.innofied.com/posts', {
            method: "POST",
            body: JSON.stringify(this.state.posts),
        })
    }
}

We have a component which has a post list in the state. We are fetching posts from some HTTP endpoint and each post is editable. This component violates the Single Responsibility Principle due to has more than 1 reason to change.

I can see these reasons for changing:

  1. Every time I want to change the header of apps.
  2. Every time I want to add a new component to the apps (e.g. footer).
  3. Every time I want to change the post fetching mechanism, for example, the address of endpoint or protocol.
  4. Every time I want to change the post list table (e.g. column styling, etc…)

Solution. After identification the reasons to change let’s try to eliminate them by creating a suitable abstraction (component, function, …) for each reason.

Let’s try to fix that problem by refactoring of the App component.


class App extends Component {

    render() {
        return (
            <div className="App">
                <Header/>
                <PostList/>
            </div>
        );
    }

}

Now, we segregate the components as per the need of change. So we solved problem 1 (change the header of application) and 2 (add a new component to the application) by moving that logic from the App component to the new components.

Let’s try to solve problem 3 and 4.


class PostList extends Component {
  
    static propTypes = {
        fetchPosts: PropTypes.func.isRequired,
        savePosts: PropTypes.func.isRequired
    };

    state = {
        posts: [{name: 'Top 10 React Native Best Practices', description: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.'}]
    };

    componentDidMount() {
        const posts = this.props.fetchPosts();
        this.setState({posts});
    }

    render() {
        return (
            <div>
                <PostTable posts={this.state.posts} onPostChange={(post) => this.updatePost(post)}/>
                <button onClick={() => this.savePosts()}>Save</button>
            </div>
        );
    }

    updatePost(post) {
      // update post in the state
    }

    savePosts(row) {
        this.props.savePosts(this.state.posts);
    }
}

This is our new container component PostList. We solved problem 3 (change the post fetching mechanism) by creating props functions fetchPost and savePost. So when we want to change the HTTP endpoint we go to the function (save/fetch)Post and change that there.

The last problem 4 (change the post list table) was resolved by creating a simple presentation component PostTable which encapsulated the HTML and styling of the post table.

2. Open Closed Principle

This principle tells that “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification

If you look at the PostList component above you can notice that if we want to display posts in a different format, we have to modify the PostList’s render method. This is a violation of this principle.

We can use this principle by using Component Composition.

Look at the refactored PostList component below.


export class PostList extends Component {

    static propTypes = {
        fetchPosts: PropTypes.func.isRequired,
        savePosts: PropTypes.func.isRequired
    };

    state = {
        posts: [{_id: 1, name: 'Top 10 React Native Best Practices', description: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.'}]
    };

    componentDidMount() {
        const posts = this.props.fetchPosts();
        this.setState({posts});
    }

    render() {
        return (
            <div>
                {this.props.children({
                    posts: this.state.posts,
                    savePosts: this.savePosts,
                    onPostChange: this.onPostChange
                })}
            </div>
        );
    }

    savePosts = () => {
        this.props.savePosts(this.state.posts);
    };

    onPostChange = (post) => {
        // change post in the state
    };
}

We modified the PostList component in a way that it is open for extension because it renders its children and therefore it is easy to extend its behavior. It is closed for modification because all necessary changes will be implemented in different components and we can even deploy this component independently.

Let’s look at how would we display posts in a list by using our new component.


export class PopulatedPostList extends Component {

    render() {
        return (
            <div>
                <PostList>{
                    ({posts}) => {
                        return <ul>
                            {posts.map((post, index) => <li key={index}>{post._id}: {post.name} {post.description}</li>)}
                        </ul>
                    }
                }
                </PostList>
            </div>
        );
    }
}

We extended the PostList behavior by creating a new component which knows how to display posts. We can even fetch new detailed information about each post in this new component without even touching PostList component and that was the goal.

3. Liskov Substitution Principle

It describes that objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.

Look at the example below.


class Developer {
  constructor(roles) {
    this.roles = roles;
  }
  
  getRoles() {
    return this.roles;
  }
}

class ReactDeveloper extends Developer {}

const javascriptDeveloper = new Developer(['js']);

const reactDeveloper = new ReactDeveloper({role: 'js'}, {role: 'react'});

function showDevRoles(dev) {
  const roles = dev.getRoles();
  roles.forEach((role) => console.log(role));
}

showDevRoles(javascriptDeveloper);

showDevRoles(reactDeveloper);

We have a class Developer which accept roles in the constructor. Then, we created an ReactDeveloper class which is the Developer derivative.

After that, we have created a simple function showDevRoles which accept dev as a parameter and prints all of the dev’s roles to the console.

But after we called the showDevRoles function with javascriptDeveloper and reactDeveloper instances, it crashed.

And why? The ReactDeveloper looks like the Developer. It definitely “quacks” like the Developer because it has the same methods. The problem was with the “batteries”. Because, when creating the react developer, we created an object of roles instead of an array.

We violated the Liskov Substitution Principle because the showDevRoles function should work correctly with the Developer and its derivatives!

We can just create an array of roles instead of an object to fix this.


const javascriptDeveloper = new Developer(['js']);
const reactDeveloper = new reactDeveloper(['js','react']);

4. Interface Segregation Principle

This principle describes us that we should not depend on things we don’t need.

This principle applies especially on static types languages because the dependencies are explicitly defined by interfaces.


class PostTable extends Component {
    ...
    
    render() {
        const post = {_id: 1, name: 'Top 10 React Native Best Practices', description: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.'};
        return (
            <div>
                ...
                  <PostRow post={post}/>
                ...
            </div>
        );
    }
   
    ...
}

class PostRow extends Component {

    static propTypes = {
        post: PropTypes.object.isRequired,
    };

    render() {
        return (
            <tr>
                <td>Id: {this.props.post._id}</td>
                <td>Name: {this.props.post.name}</td>
            </tr>
        )
    }

}

PostTable component renders PostRow component while passing whole post object to its props. When you look at the PostRow component, it depends on the whole post but only cares about post’s id and name.

When you would write a test for this component in Typescript or Flow, you have to mock the whole post because otherwise, your compiler will fail.

At first, it does not seem a problem if you are using plain Javascript, but someday, you will add Typescript to your code base and suddenly it will break all tests cases because you would have to assign all properties from interfaces even if you are using only half of them.

But still, it is more descriptive in this way.


class PostTable extends Component {
    ...
    
    render() {
        const post = {_id: 1, name: 'Top 10 React Native Best Practices', description: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.'};
        return (
            <div>
                ...
                  <PostRow id={post._id} name={post.name}/>
                ...
            </div>
        );
    }
   
    ...
}

class PostRow extends Component {

    static propTypes = {
        id: PropTypes.number.isRequired,
        name: PropTypes.string.isRequired,
    };

    render() {
        return (
            <tr>
                <td>Id: {this.props.id}</td>
                <td>Name: {this.props.name}</td>
            </tr>
        )
    }

}

Note: Keep in mind that this principle does not apply only on prop-types.

5. Dependency Inversion Principle

This principle states that we should depend upon abstractions, not concretions.

Let’s look at the below example.


class App extends Component {
  
  ...

  async fetchPosts() {
    const users = await fetch('http://api.innofied.com/posts');
    this.setState({posts});
  }

  ...
}

If you look at this code you will see that component App depends on the concretion, more specifically global posts fetch. As per UML, this relation would look like this.

A high-level module shouldn’t rely on low-level details, each ought to rely on abstraction. The App should not know how to fetch posts. In order to fix that problem, we have to inverse the dependencies between App component and fetch Post so the UML diagram will look like this.

And the implementation is here.


class App extends Component {
  
    static propTypes = {
        fetchPosts: PropTypes.func.isRequired,
        savePosts: PropTypes.func.isRequired
    };

    ...
    
    componentDidMount() {
        const posts = this.props.fetchPosts();
        this.setState({posts});
    }

    ...

}

We can tell that App is loosely coupled because it has no knowledge whether we are using HTTP, SOAP. In other words, it does not care.

It gives us power because we can easily change the post fetching method and the App component will not change a bit!

And testing is really simple, we can easily mock these post fetching functions.

Summary

These principles help us to write better code and reduce bugs in the future. Always remember “Time change But Principles don’t change”. So let’s do Happy Coding.

Goutam Singha

Goutam Singha is working as a full stack web developer. He uses ReactJS, Redux in front-end development and NodeJS in back-end development with database MongoDB. He used to javascript framework – AngularJS, Angular 2/4/5, Ionic 1/2/3 and gain expertise in the cross-platform mobile app and web app development. Apart from that, he loves to work in an agile environment for which he is a quick learner and adopter with latest technologies and trends.