Chidume Nnamdi I'm a software engineer with over six years of experience. I've worked with different stacks, including WAMP, MERN, and MEAN. My language of choice is JavaScript; frameworks are Angular and Node.js.

SOLID principles: Single responsibility in JavaScript frameworks

12 min read 3503

SOLID Principles: Single Responsibility in JavaScript Frameworks

The single responsibility principle is one of five object-oriented design (OOD) guidelines that comprise the SOLID design principles.

In this tutorial, we’ll focus on the single-responsibility principle and demonstrate how it can help guide your design decisions in JavaScript frameworks, especially Angular and React.

Here’s what we’ll cover:

What are SOLID principles?

SOLID is an acronym that stands for the first five OOD principles as outlined by renowned software engineer Robert C. Martin. The SOLID principles are designed to help developers design robust, maintainable applications.

The five SOLID principles are:

  1. Single-responsibility principle
  2. Open–closed principle
  3. Liskov substitution principle
  4. Interface segregation principle
  5. Dependency inversion principle

What is the single responsibility principle?

The single responsibility principle in JavaScript deals with the cohesiveness of modules. It states that functions and classes should only have one job.

Take, for example, a Car model:

class Car {
    constructor(name,model,year) {
        this.name=name
        this.model=model
        this.year=year
    }
getCar(id) {
        return this.http.get('api/cars/'+id)
    }
saveCar() {
        return this.post('api/cars', { name: this.name, year: this.year, model: this.model })
    }
}

The above example violates the single responsibility principle. Why? The Car model was meant to hold/represent a car, but it has a getCar method that fetches a car from the internet. That gives it another responsibility of getting cars from an endpoint.

A line needs to be drawn on the responsibility of the Car class: will it be used as a model or as an object?

If we touch either the saveCar or getCar methods to make a change, this change may force us to redesign the Car model either by adding an extra property or adding another thing in the Car class. If we forget to do this, that application may break in unpredictable ways.

We can separate the responsibilities to different classes:

class Car {
    constructor(name, model, year) {
        this.name = name
        this.model = model
        this.year = year
    }
}
class CarService {
    getCar(id) {
        return this.http.get('api/cars/'+id)
    }
    saveCar(car) {
        this.http.post('api/cars', car)
    }
}

As you can see from this example, we now have the responsibilities separated. Now, the Car model manages a car and the CarService has the responsibility of getting and saving cars from an endpoint.

If a class has more than one responsibility, the responsibilities become coupled. Changes to one responsibility may inhibit the class’s ability to meet the others. This kind of coupling leads to fragile designs that break in unexpected ways when changed.

The examples below show how to use the single responsibility principle in React and Angular components. These examples are also applicable in other JavaScript frameworks, such as Vue.js, Svelte, etc.

The single responsibility principle in React

Let’s say we have the following React component:

class Movies extends Component {
    componentDidMount() {
        store.subscribe(() => this.forceUpdate())
    }
render() {
        const state = store.getState()
        const movies = state.movies.map((movie, index) => {
                <div className="movie-card" key={index}>
                    {{movie.name}}
                    Year: {{movie.year}}
                    Gross: {{movie.gross}}
                </div>
        })
        return (
            <div>
                <div className="movie-header">Movies App</div>
                <div className="movies-list">
                    {movies} 
                </div>
            </div>
        )
    }
}

This component has a handful of issues:

  • State management — The component subscribes to the store
  • Data fetching — It gets the state from the store
  • UI presentation — It renders the movies list
  • Business logic — It’s tied to the business logic of the application (the logic on how to get movies)

This React component is not reusable. If we want to re-use the movies list in another component in the app — for example, a component that displays high-grossing movies, movies by year, etc. — we must rewrite the code in each component, even though they’re the same.

This component will be hard to maintain because it contains so many parts. There will be breaking changes if one part changes. It cannot be optimized, it produces side effects, and we can’t effectively memoize the React component for performance because doing so would result in stale data.

Separating concerns in a React component

Continuing our React component example above, we need to extract the UI presentation from the Movies component.

We’ll create another component, MoviesList, to deal with this. The MoviesList component will expect the movies array from its props:

class MoviesList extends Component {
    render() {
        const movies = props.movies.map((movie, index) => {
                <div className="movie-card" key={index}>
                    {{movie.name}}
                    Year: {{movie.year}}
                    Gross: {{movie.gross}}
                </div>
        })
        return (
            <div className="movies-list">
                {movies} 
            </div>
        )
    }
}
class Movies extends Component {
    componentDidMount() {
        store.subscribe(() => this.forceUpdate())
    }
render() {
        const state = store.getState()
        const movies = state.movies        return (
            <div>
                <div className="movie-header">Movies App</div>
                <MoviesList movies={movies} />
            </div>
        )
    }
}

We refactored the Movies component and decoupled the UI presentation code from it. Now it’s only concerned with how to subscribe to the store, get the movies data from the store, and pass it to the MoviesList component. It’s no longer concerned about how to render the movies; that is now the responsibility of the MoviesList component.

The MoviesList component is the presentational component. It only presents the movies given to it via the movies props. It doesn’t care where the movies are gotten from, whether from the store, localStorage, or a dummy server/dummy data, etc.

With these, we can reuse the MoviesList component anywhere in our React app or even in other projects. This React component can be share with the Bit cloud to enable other users around the world to use the component in their projects.



The single responsibility principle in Angular

Angular apps are composed of components. A component holds a single view composed of elements.

Components make it easier to build complex apps from a single, simple unit of view. Instead of diving headfirst into building complex apps, components enable you to break it down and compose the app from small units.

For example, let’s say you want to build a Facebook-like social media app. You can’t just create HTML files and pour in elements. You’d need to break it down into small units of view to organize your HTML files in a structure that looks something like this:

  • Feed page
  • Profile page
  • Registration page
  • Login page

Each file will be composed of components. For example, the feed page will consist of feeds from our friends, comments, likes, and shares, to name a few. All these need to be handled individually.

If we compose these into components, we have a FeedList component that takes an array of feeds fetched from an API and a FeedView component that to handle the display of the data feeds.

When building a new Angular application, start by:

  1. Breaking down the application into separate components
  2. Describe each component’s responsibilities
  3. Describe each component’s inputs and outputs — i.e., its public-facing interface.

Most of the components we write violate the single responsibility principle. Let’s say, for example, we have an app that lists movies from an endpoint:

@Component({
    selector: 'movies',
    template: `
        <div>
            <div>
                <div *ngFor="let movie of movies">
                    <h3>{{movie.name}}</h3>
                    <h3>{{movie.year}}</h3>
                    <h3>{{movie.producer}}</h3>
                    <button (click)="delMovie(movie)">Del</button>
                </div>
            </div>
        </div>
    `
})
export class MoviesComponent implements OnInit {
    this.movies = []
    constructor(private http: Http) {}
ngOnInit() {
        this.http.get('api/movies/').subscribe(data=> {
            this.movies = data.movies
        })
    }
delMovie(movie) {
        // deletion algo
    }
}

This component is responsible for:

  • Fetching the movies from the api/movies API
  • Managing the array of movies

This is bad for business. Why? This component should either be responsible for one task or the other; it can’t be responsible for both.

The point of assigning each component a single responsibility is to make it reusable and optimizable. We need to refactor our example component to push some responsibilities to other components. Another component needs to handle the movies array, and the data-fetching logic should be handled by a Service class.

@Injectable() {
    providedIn: 'root'
}
export class MoviesService {
    constructor(private http: Http) {}
getAllMoives() {...}
    getMovies(id) {...}
    saveMovie(movie: Movie) {...}
    deleteMovie(movie: Movie) {...}
}
@Component({
    selector: 'movies',
    template: `
        <div>
            <div>
                <movies-list [movies]="movies"></movies-list>
            </div>
        </div>
    `
})
export class MoviesComponent implements OnInit {
    this.movies = []
    constructor(private moviesService: MoviesService) {}
ngOnInit() {
        this.moviesService.getAllMovies().subscribe(data=> {
            this.movies = data.movies
        })
    }
}
@Component({
    selector: 'movies-list',
    template: `
        <div *ngFor="let movie of movies">
            <h3>{{movie.name}}</h3>
            <h3>{{movie.year}}</h3>
            <h3>{{movie.producer}}</h3>
            <button (click)="delMovie(movie)">Del</button>
        </div>
    `
})
export class MoviesList {
    @Input() movies = null
delMovie(movie) {
        // deletion algo
    }
}

Here, we separated the multiple concerns in the MoviesComponent. Now, MoviesList handles the array of movies and the MoviesComponent is now its parent that sends the movies array to MoviesList via movies input. The MoviesComponent doesn’t know how the array will be formatted and rendered; that’s up to the MoviesList component. The sole responsibility of MoviesList is to accept a movies array via its movies input and display/manage the movies.

Let’s say we want to display recent movies or related movies in a movie profile page. We can reuse the movies list without writing a new component for it:

@Component({
    template: `
    <div>
        <div>
            <h3>Movie Profile Page</h3>
            Name: {{movie.name}}
            Year: {{movie.year}}
            Producer: {{movie.producer}}        
        </div>
<br />
<h4>Movie Description</h4>
        <div>
            {{movie.description}}
        </div>
        <h6>Related Movies</h6>
        <movies-list [movies]="relatedMovies"></movies-list>
    </div>    
    `
})
export class MovieProfile {
    movie: Movie = null;
    relatedMovies = null;
    constructor(private moviesService: MoviesService) {}
}

Since our MoviesComponent is used to display movies in the main page of our application, we can reuse the MovieList in the sidebar to display trending movies, highest-rated movies, highest-grossing movie, best anime movies, etc. No matter what, the MovieList component can fit in seamlessly. We can also add an extra property to the Movie class and it won’t break our code where we use the MovieList component.

Next, we moved the movies data-fetching logic to a MoviesService. This service deals with any CRUD operations on our movies API.

@Injectable() {
    providedIn: 'root'
}
export class MoviesService {
    constructor(private http: Http) {}
getAllMovies() {...}
    getMovies(id) {...}
    saveMovie(movie: Movie) {...}
    deleteMovie(movie: Movie) {...}
}

The MoviesComponent injects the MoviesService and calls any method it needs. One benefit of separation of concerns is that we can optimize this class to prevent wasted renders.

Change detection in Angular starts from the root component or from the component that triggers it. MoviesComponent renders MovieList; whenever a CD is run, the MoviesComponent is rerendered, followed by the MovieList. Rerendering a component might be wasteful if the inputs didn’t change.

Think of MoviesComponent as a smart component and MovieList as a dumb component. Why? Because MoviesComponent fetches the data to be rendered, but the MovieList receives the movies to be rendered. If it doesn’t receive anything, it renders nothing.

Smart components cannot be optimized because they have/cause unpredictable side effects. Trying to optimize them will cause the wrong data to display. Dumb components can be optimized because they are predictable; they output what they are given and their graph is linear. A smart component’s graph is like a fractal curve with countless anomalies differences.

In other words, smart components are like impure functions and dumb components are pure functions, like reducers in Redux. We can optimize the MovieList component by adding the changeDetection to OnPush:

@Component({
    selector: 'movies-list',
    template: `
        <div *ngFor="let movie of movies">
            <h3>{{movie.name}}</h3>
            <h3>{{movie.year}}</h3>
            <h3>{{movie.producer}}</h3>
            <button (click)="delMovie(movie)">Del</button>
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class MoviesList {
    @Input() movies = null
delMovie(movie) {
        // deletion algo
    }
}

MovieList will rerender only when:

  • The movies array input changes
  • The Del button is clicked

Check it. If the previous value of movies is:

[
    {
        name: 'MK',
        year: 'Unknown'
    }
]

And the current value is:

[
    {
        name: 'MK',
        year: 'Unknown'
    },
    {
        name: 'AEG',
        year: '2019'
    }
]

The component needs to rerender to reflect the new changes. When we click the Del button, rerendering will take place. Here, Angular doesn’t start rerendering from the root; it starts from the parent component of the MovieList component. This is because we’re removing a movie from the movies array so the component should rerender to reflect the remaining array. This component deletes a movie from its movies array, which might limit its reusability.

What happens if a parent component wants to delete two movies from the array? We would see that touching the MovieList to adapt to the change would violate the single responsibility principle.

It shouldn’t actually delete a movie from its array. It should emit an event that would cause the parent component to pick up the event, delete a movie from its array, and pass back the remaining values in the array to the component.

@Component({
    selector: 'movies-list',
    template: `
        <div *ngFor="let movie of movies">
            <h3>{{movie.name}}</h3>
            <h3>{{movie.year}}</h3>
            <h3>{{movie.producer}}</h3>
            <button (click)="delMovie(movie)">Del</button>
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class MoviesList {
    @Input() movies = null
    @Output() deleteMovie = new EventEmitter()
delMovie(movie) {
        // deletion algo
        this.deleteMovie.emit(movie)
    }
}

So with this, the parent component can emit two events if it wants to delete two movies.

@Component({
    selector: 'movies',
    template: `
        <div>
            <div>
                <movies-list [movies]="movies" (deleteMovie)="delMovie"></movies-list>
            </div>
        </div>
    `
})
export class MoviesComponent implements OnInit {
    this.movies = []
    constructor(private moviesService: MoviesService) {}
ngOnInit() {
        this.moviesService.getAllMovies().subscribe(data=> {
            this.movies = data.movies
        })
    }
    delMovie() {
        this.movies.splice(this.movies.length,2)
    }
}

As you can see, dumb components rerender based on the parent component and user interactions, which is predictable and, therefore, optimizable.

Smart components can be optimized by adding the OnPush change detection strategy:

@Component({
    selector: 'movies',
    template: `
        <div>
            <div>
                <movies-list [movies]="movies"></movies-list>
            </div>
        </div>
    `,
    changeDetection: ChangeDetctionStrategy.OnPush
})
export class MoviesComponent implements OnInit {
    this.movies = []
    constructor(private moviesService: MoviesService) {}
ngOnInit() {
        this.moviesService.getAllMovies().subscribe(data=> {
            this.movies = data.movies
        })
    }
}

But this leads to side effects that could cause it to trigger numerous times, rendering the OnPush strategy totally useless.

Dumb components should make up the bulk of your application because they are optimizable and thus conducive to high performance. Using too many smart components can make the app slow because they are not optimizable.

Single responsibility: Side effects

Side effects can occur when the app state changes from a certain point of reference. How does it affect performance?

Let’s say we have these functions:

let globalState = 9
function f1(i) {
    return i * 90
}
function f2(i) {
    return i * globalState
}
f1 can be optimized to stop running when the input is the same as prev, but f2 cannot be optimized because it is unpredictable, it depends on the globalState variable. It will store its prev value but the globalState might have been changed by an external factor it will make optimizing f2 hard. f1 is predictable because it doesn't depend on an outside variable outside its scope.

Side effects in React

Side effects can lead to stale data or inaccurate data in React. To prevent that, React provides a useEffect Hook that we can use to perform our side effects in its callback.

function SmartComponent() {
  const [token, setToken] = useState('')
  useEffect(() => {
    // side effects code here...
    const _token = localStorage.getItem("token")
    setToken(token)
  })
  return (
    <div>
      Token: {token}
    </div>
  )
}

Here, we’re getting external data using localStorage, which is a side effect. This is done inside the useEffect hook. The callback function in the useEffect hook is called whenever the component mounts/updates/unmounts.

We can optimize the useEffect Hook by passing a second argument called the dependency array. The variables are what useEffect checks on each update to know whether to skip running on a rerender.

Side effects in Angular

Smart components, when optimized with OnPush, result in data inaccuracy.

Take our MoviesComponent, for example. Let’s say we optimize with OnPush and have an input that receives certain data.

@Component({
    template: `
        ...
        <button (click)="refresh">Refresh</button>
    `,
    changeDetection: ChangeDetectionStartegy.OnPush
})
export class MoviesComponent implements OnInit {
    @Input() data = 9
    this.movies = []
    constructor(private moviesService: MoviesService) {}
ngOnInit() {
        this.moviesService.getAllMovies().subscribe(data=> {
            this.movies = data.movies
        })
    }
refresh() {
        this.moviesService.getAllMovies().subscribe(data=> {
            this.movies = data.movies
        })        
    }
}

This component causes a side effect by performing an HTTP request. This request changes data in the movies array inside the component and needs to render the movies array. Our data is of value 9. When this component rerenders, perhaps by clicking a button that causes the refresh method to run, an HTTP request will occur to fetch a new array of movies from the network and a ChangeDetection is run on this component. If the @Input() data of this component doesn’t change from its parent, this component won’t rerender, resulting in an inaccurate display of the movies array. The previous movies are displayed, but new movies are also fetched.

Now you’ve seen the effects of side effects. A component that causes side effects is unpredictable and hard to optimize.

Side effects include:

  • HTTP requests
  • Global state change (in Redux)

ngrx effects

ngrx is a collection of reactive extensions for Angular. As we’ve seen, our components are service-based. Components inject services to perform different operations from network requests to provide state. These services also inject other services to work, which will cause our components to have different responsibilities.

Like in our MoviesComponent, it injected the MoviesService to perform CRUD operations on the movies API.

This service also injects the HTTP service class to help it perform network requests. This makes our MoviesComponents dependent on the MoviesService class. If the MoviesService class makes a breaking change, it may affect our MoviesComponent. Just imagine your app growing to hundreds of components injecting the service; you’d find yourself scouring through every component that injects the service to refactor them.

Many store-based applications incorporate the RxJS-powered side effect model. Effects relieve our components of numerous responsibilities.

To show an example, let’s have MoviesComponent use effects and move the movies data to Store:

@Component({
    selector: 'movies',
    template: `
        <div>
            <div>
                <movies-list [movies]="movies | async"></movies-list>
            </div>
        </div>
    `
})
export class MoviesComponent implements OnInit {
    movies: Observable<Movies[]> = this.store.select(state => state.movies)
constructor(private store: Store) {}
ngOnInit() {
        this.store.dispatch({type: 'Load Movies'})
    }
}

There is no more MoviesService; it has been delegated to the MoviesEffects class:

class MoviesEffects {
    loadMovies$ = this.actions.pipe(
        ofType('Load Movies'),
        switchMap(action =>
            this.moviesService.getMovies()
            .map(res => ({ type: 'Load Movies Success',payload: res }))
            .catch(err => Observable.of({ type: 'Load Movies Failure', payload: err }))
            );
    )
constructor(private moviesService: MoviesService, private actions: Actions) {}
}

The service MoviesService is no longer the responsibility of the MoviesComponent. Changes to MoviesService will not affect MoviesComponent.

Container and presentational components

Container components are self-contained components that can generate and render their own data. A container component is concerned with how its internal operations work within its own sandbox boundaries.

According to Oren Farhi, a container component is smart enough to perform a few operations and make some decisions:

  1. It is often responsible for fetching data that might be displayed
  2. It might be composed of several other components
  3. It is “stateful,” which means it  may manage a certain state
  4. It handles the internal components’ events and async operations

Container components are also called smart components.

Presentational components get their data from their parent. If they get no input from the parent, they’ll display no data. They are dumb in that they can’t generate their own data; this is dependent on the parent.

Conclusion

We delved deep into making our components in React/Angular reusable. It’s not only about writing code or knowing how to code, but it’s knowing how to code well.

Don’t start by building complex things; compose them from small components. The single responsibility principle helps ensure that we write clean and reusable code.

: Debug JavaScript errors more easily by understanding the context

Debugging code is always a tedious task. But the more you understand your errors the easier it is to fix them.

LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to find out exactly what the user did that led to an error.

LogRocket records console logs, page load times, stacktraces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!

.
Chidume Nnamdi I'm a software engineer with over six years of experience. I've worked with different stacks, including WAMP, MERN, and MEAN. My language of choice is JavaScript; frameworks are Angular and Node.js.

5 Replies to “SOLID principles: Single responsibility in JavaScript frameworks”

  1. Did you know that SRP is in fact an organizational pattern, related to teams and people more than to code, according to Robert C. Martin?

    1. It’s awesome article about SRP with perfect example, it will help so many developers to think to do the code in angular with SRP , it’s cool 😎 , thanks a lot for your time to invest on this article

  2. Whose aim is to be used in code, so as to make code readable and maintainable. Thanks.

Leave a Reply