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:
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:
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.
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:
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.
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.
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:
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:
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:
api/movies
APIThis 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:
Del
button is clickedCheck 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.
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 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.
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:
ngrx
effectsngrx
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 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:
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.
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.
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 see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowExplore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.
5 Replies to "SOLID principles: Single responsibility in JavaScript frameworks"
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?
Great post. Very helpful.
Thanks ❤️❤️❤️
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
Whose aim is to be used in code, so as to make code readable and maintainable. Thanks.