Putting the `S` in SOLID JavaScript

🕑 2 minute read

S - Single-Responsibility Principle (SRP) is the first design principal covered by SOLID principals.

SOLID is an Object oriented design principal. It stands for:

  • S - Single-responsiblity Principle
  • O - Open-closed Principle
  • L - Liskov Substitution Principle
  • I - Interface Segregation Principle
  • D - Dependency Inversion Principle

The principals were originally created and promoted by Robert Martin (aka Uncle Bob).

SRP simply states that Classes and functions should only have a single responsibility. This sounds fairly easy to do in principle, but in practice, especially in React it can be slightly more nuanced. Let me explain with a class that handles an Artist and the artist's details.

// Wrong!
class Artist {
    constructor(id) {
        this.id = id;
        this.getBio()
        this.getSingles();
        this.getAlbums()
    }

    getBio() {
        fetch(`/artist/${this.id}/bio/`)
       //... do stuff with data
    }

    getSingles() {
        fetch(`/artist/${this.id}/singles/`)
        //... do stuff with data
    }

    getAlbums() {
        fetch(`/artist/${this.id}/articles/`)
        //... do stuff with data
    }
}

const theArtist = new Artist('formerlyKnownAsPrince');

In this example the Artist class is responsible for multiple things. It should just represent an artist, but it also has the responsibility to get the bio, the singles and the albums.

Why is this bad? For example, if we ever need to update getBio to have an extra property, such as this.fetchedData = true we could end up breaking other parts of the class. We could be reeeeeally careful and maybe use more specific variables like this.fetchedBio but the more complex our application and classes/functions get the harder it will be to manage and track this. Instead, we can split the responsibilities up and create different classes and funcions that only handle one responsibility.

// Right!

function getBio(id) {
    fetch(`/artist/${id}/bio/`)
    //... do stuff with data
    return bio;
}

function getSingles(id) {
    fetch(`/artist/${id}/singles/`)
    //... do stuff with data
    return singles;
}

function getAlbums(id) {
    fetch(`/artist/${this.id}/articles/`)
    //... do stuff with data
    return albums;
}

class Artist {
    constructor(bio, singles, albums) {
        this.bio = bio;
        this.singles = singles;
        this.albums = albums;
    }
}

function getArtistDetails(id) {
    const bio = getBio(id);
    const singles = getSingles(id);
    const albums = getAlbums(id);

    return { bio, singles, albums };
}

const { bio, singles, albums } = getArtistDetails('formerlyKnownAsPrince')

const theArtist = new Artist(bio, singles, albums);

Now we have split up the Artist class and moved the methods to pure functions. Each one has a single responsibility.

But what about the getArtistDetails function being responsible for getting the 3 things (bio, singles and albums)? Well this is ok because that it's really 1 responsibility - to get the data!

Now if we want to save that artist, then we can create a new class - Saver which can have just one responsibility too! We don't have to worry about what other methods are on the Artist or what properties it may have. We can just take an Artist and save it without worrying about breaking other properties or methods.

//... the rest
const theArtist = new Artist(bio, singles, albums); // from before

class Saver {
    save (data) {
        // something to save the data
    }
}

const saver = new Saver();

saver.save(theArtist);

React

The same principles apply to react, whether you are creating component classes or functional components.

// Wrong
const Artist = ({ id }) => {
    const [bio, setBio] = useState(null);
    const [singles, setSingles] = useState([]);
    const [albums, setAlbums] = useState([]);

    useEffect(()=> {
        fetch(`/artist/${id}/bio/`)
            .then(data => {
                setBio(data);
            })
    }, [id])

    useEffect(()=> {
        fetch(`/artist/${id}/singles/`)
            .then(data => {
                setSingles(data);
            })
    }, [id])

    useEffect(()=> {
        fetch(`/artist/${id}/albums/`)
            .then(data => {
                setAlbums(data);
            })
    }, [id])

    const singlesList = singles.map(single => (
        <li key={single.id}>{single.title} - {single.releaseDate}</li>
    ));

    const albumsList = albums.map(album => (
        <li key={album.id}>{album.title} <img src={album.artWorkUrl} /></li>
    ));

    return (
        <div>
            { bio ? <h2>{bio.name}</h2> : null }
            <ul>{singlesList}</ul>
            <ul>{albumsList}</ul>
        </div>
    )
}

We have a few things going on that are wrong with the component. There is data retrieval/state logic and there is presentational logic all in the same component. We have the rendering of each list withing the main component. It makes the component very hard to reuse, or change for other uses because it's doing so many things.

Let's fix it! We will separate the data logic from the presentational UI and logic. Each part of the UI has it's own dedicated single responsibility component.

const Bio = ({bio}) => {
    if (!bio) return null;

    return <h2>{ bio.name}</h2>;
}

const SingleList = ({singles}) => {
    const singlesList = singles.map(single => (
        <li key={single.id}>{single.title} - {single.releaseDate}</li>
    ));

    return (
        <ul>
            {singlesList}
        </ul>
    )
}

const AlbumList = ({albums}) => {
    const albumsList = albums.map(album => (
        <li key={album.id}>{album.title} <img src={album.artWorkUrl} /></li>
    ));

    return (
        <ul>
            {albumsList}
        </ul>
    )
}

const ArtistCard = ({bio, singles, albums}) => (
    <div>
        <Bio bio={bio} />
        <SingleList singles={singles} />
        <AlbumList albums={albums} />
    </div>
)

const Artist = ({ id }) => {
    const [bio, setBio] = useState(null);
    const [singles, setSingles] = useState([]);
    const [albums, setAlbums] = useState([]);

    useEffect(()=> {
        fetch(`/artist/${id}/bio/`)
            .then(data => {
                setBio(data);
            })
    }, [id])

    useEffect(()=> {
        fetch(`/artist/${id}/singles/`)
            .then(data => {
                setSingles(data);
            })
    }, [id])

    useEffect(()=> {
        fetch(`/artist/${id}/albums/`)
            .then(data => {
                setAlbums(data);
            })
    }, [id])

    return <ArtistCard bio={bio} singles={singles} albums={albums} />
}

Now it means that we can reuse the UI in other places, for example we could use the ArtistCard for a list of artists. We didn't need to recreate the same UI or handle any logic. It just works™️.

const ArtistList = ({listOfArtists}) => {
    const artists = listOfArtists.map(({bio, singles, albums}) => (
        <ArtistCard bio={bio} singles={singles} albums={albums} />
    ));

    return <div>{artists}</div>;
}

Because the components we have created are all pure (given the same input they will return the same value) this means that we gain 2 very useful benefits.

  1. We can optimise them easily with react memo since the same input returns the same output.
  2. Testing is easy because, again, the output is always the same given the same input, and we don't need to depend on the data source, so we can supply our own data any way we want to mock it!

Summary

Single-Responsibility Principle is a great fundamental principle to learn and use because it makes your Classes, functions and components more reliable, reusable, optimisable and testable.

Just remember to:

  1. Check every function you create to see if there are any smaller parts that can be moved in to single use functions. 2.Check your functions to make sure they are not doing 2 or more different things.
  2. Repeat