What's inside
In my work as a front-end developer, I often encounter single methods, classes, and React components doing way too much. These blocks of code are typically long, use many variables, and allow complex execution paths. Pieces of code performing distinct tasks are often coupled together. That inhibits code reusability, makes testing cumbersome, and increases the time required to understand the code.
That’s where the Single Responsibility Principle comes in handy.
Why the Single Responsibility Principle?
The Single Responsibility Principle simply means that a method, a class, or a module has only one responsibility.
For example:
The responsibility of a method may be adding two numbers. The responsibility of a class may be providing a calculator interface. The responsibility of a module may be connecting its internal parts together.
Applying the Single Responsibility Principle often leads to developing smaller pieces of code where each is focused on just one task. Then we can have these pieces cooperate together to perform more complex operations.
Remember that breaking code into small pieces isn’t always the best way to go. It depends on the project and specific circumstances. Sometimes making things reusable brings no benefits at all - for instance, in a small project or script where we just know that we won’t be reusing any of its elements. That’s why before following the Single Responsibility Principle we need to decide what pays off in particular circumstances.
Yet, in most software projects keeping the Single Responsibility Principle is crucial. Why? Because it reduces time and cost of introducing changes, bug fixes, and new functionalities.
In this series of blog posts, I will go through an example of refactoring a fat React component into smaller pieces: a service, utility functions, and other components, and comment on the benefits each step of that process brings.
This post series assumes that readers have some knowledge of React, including the higher-order components, and ECMAScript 2015 features like arrow functions, classes, promises, and symbols.
Refactoring a React component
Let's start right away with the code. Have a look at the code below and check how much time you need to understand what this component does.
src/components/LatestReactPullRequest/LatestReactPullRequest.js
import React, { Component, Fragment } from 'react';
import moment from 'moment';
import makePromiseCancelable from '@utils/makePromiseCancelable';
import './LatestReactPullRequest.css';
const DATA_FETCHING_STATUS = {
NOT_STARTED: Symbol('DATA_FETCHING_NOT_STARTED'),
IN_PROGRESS: Symbol('DATA_FETCHING_STATUS_IN_PROGRESS'),
SUCCESS: Symbol('DATA_FETCHING_STATUS_SUCCESS'),
FAILURE: Symbol('DATA_FETCHING_STATUS_FAILURE'),
};
class LatestReactPullRequest extends Component {
constructor(props) {
super(props);
this.state = {
dataFetchingStatus: DATA_FETCHING_STATUS.NOT_STARTED,
data: null,
};
}
componentDidMount() {
this.setState({
dataFetchingStatus: DATA_FETCHING_STATUS.IN_PROGRESS,
});
const fetchPromise = fetch('https://api.github.com/repos/facebook/react/pulls?state=all&sort=created&direction=desc&per_page=1&page=1', {
headers: {
'Accept': 'application/vnd.github.v3+json',
}
});
const { promise, cancel } = makePromiseCancelable(fetchPromise);
this.cancelDataFetchingPromise = cancel;
promise
.then((response) => {
if (response.status !== 200) {
throw new Error(\`Response status code: ${response.status}\`);
} else {
response.json()
.then(([latestReactPullRequest]) => {
this.setState({
dataFetchingStatus: DATA_FETCHING_STATUS.SUCCESS,
data: {
title: latestReactPullRequest.title,
body: latestReactPullRequest.body,
userLogin: latestReactPullRequest.user.login,
createdAt: latestReactPullRequest.created_at,
}
});
});
}
})
.catch((error) => {
if (error.isCanceled) return;
this.setState({
dataFetchingStatus: DATA_FETCHING_STATUS.FAILURE,
data: null,
});
});
}
componentWillUnmount() {
this.cancelDataFetchingPromise();
}
render() {
const { dataFetchingStatus, data } = this.state;
return (
<div className="c-latest-react-pull-request">
{dataFetchingStatus === DATA_FETCHING_STATUS.NOT_STARTED && (
<Fragment>Initializing...</Fragment>)
}
{dataFetchingStatus === DATA_FETCHING_STATUS.IN_PROGRESS && (
<Fragment>Fetching...</Fragment>
)}
{dataFetchingStatus === DATA_FETCHING_STATUS.FAILURE && (
<Fragment>Data fetching error...</Fragment>
)}
{dataFetchingStatus === DATA_FETCHING_STATUS.SUCCESS && (
<Fragment>
<div className="c-latest-react-pull-request__title">{data.title}</div>
<div className="c-latest-react-pull-request__body">{data.body}</div>
<div className="c-latest-react-pull-request__created-at-and-user-login">
{moment(data.createdAt).calendar()} by {data.userLogin}
</div>
</Fragment>
)}
</div>
);
}
}
export default LatestReactPullRequest;
Component's main tasks
The component's name reveals that it relates to the latest React pull request.
Let's list its main tasks. The component:
1. Fetches information about the most recently created pull request in React repo on GitHub,
2. Shows the current fetching status,
3. Displays the title, body, creation time, and author's login of the pull request.
The component looks self-sufficient in performing these three tasks. That may be an acceptable solution if none of these functionalities are duplicated in other places in the application and we don’t intend to change this code later.
Note that in a typical front-end application, we would probably need to reuse some of this functionality in other components.
Component's responsibilities
Let's take a look at the component's code again and see what responsibilities it has. Our component is responsible for:
1. handling the network request,
2. knowing what the URL to the React repo is,
3. knowing how to ask the GitHub API for the latest pull request,
4. treating a non-200 HTTP response to "failure",
5. extracting required data from the response,
6. monitoring the status of the fetching operation,
7. displaying the fetching status,
8. rendering the latest pull request data.
Let's distribute these responsibilities and observe what benefits it brings.
Extracting data fetching functionality
We start with extracting the data fetching functionality to a service.
First, we need to prepare the code for extraction. Right now, our code is mixed with the component's setState operations.
We can achieve that by restructuring the operations in the promise chain into two stages:
In the first stage, data is extracted from the response, packed to an object, and passed down (this code is general and decoupled from the surrounding component, it doesn't know how the data will be consumed),
In the second stage, the data and fetching status are saved to the component's state (this code is bound to the component and decoupled from the data source, i.e. it doesn't know where the data comes from).
src/components/LatestReactPullRequest/LatestReactPullRequest.js
// ...
componentDidMount() {
this.setState({
dataFetchingStatus: DATA_FETCHING_STATUS.IN_PROGRESS,
});
// stage 1
const fetchPromise = fetch('https://api.github.com/repos/facebook/react/pulls?state=all&sort=created&direction=desc&per_page=1&page=1', {
headers: {
'Accept': 'application/vnd.github.v3+json',
}
})
.then(function(response) {
if (response.status !== 200) {
throw new Error(\`Response status code: ${response.status}\`);
} else {
return response.json()
.then(function([latestReactPullRequest]) {
return {
title: latestReactPullRequest.title,
body: latestReactPullRequest.body,
userLogin: latestReactPullRequest.user.login,
createdAt: latestReactPullRequest.created_at,
}
});
}
});
// stage 2
const { promise, cancel } = makePromiseCancelable(fetchPromise);
this.cancelDataFetchingPromise = cancel;
promise
.then((data) => {
this.setState({
dataFetchingStatus: DATA_FETCHING_STATUS.SUCCESS,
data
});
})
.catch((error) => {
if (error.isCanceled) return;
this.setState({
dataFetchingStatus: DATA_FETCHING_STATUS.FAILURE,
data: null,
});
});
}
// ...
Since the code in stage 1 is now component-independent, we can extract it to a service.
src/services/github/actions/fetchLatestReactPullRequest/fetchLatestReactPullRequest.js
export default function fetchLatestReactPullRequest() {
return fetch('https://api.github.com/repos/facebook/react/pulls?state=all&sort=created&direction=desc&per_page=1&page=1', {
headers: {
'Accept': 'application/vnd.github.v3+json',
}
})
.then(function(response) {
if (response.status !== 200) {
throw new Error(\`Response status code: ${response.status}\`);
} else {
return response.json()
.then(function([latestReactPullRequest]) {
return {
title: latestReactPullRequest.title,
body: latestReactPullRequest.body,
userLogin: latestReactPullRequest.user.login,
createdAt: latestReactPullRequest.created_at,
}
});
}
});
}
src/services/github/actions/fetchLatestReactPullRequest/index.js
export { default } from './fetchLatestReactPullRequest';
src/services/github/index.js
import fetchLatestReactPullRequest from './actions/fetchLatestReactPullRequest';
export default {
fetchLatestReactPullRequest,
};
src/components/LatestReactPullRequest/LatestReactPullRequest.js
// ...
import githubService from '@services/github';
// ...
componentDidMount() {
// ...
// stage 1
const fetchPromise = githubService.fetchLatestReactPullRequest();
// stage 2
// ...
}
// ...
By extracting code to the "github" service, we have freed the component from the task of performing all the micro operations required to fetch the data. Now it just calls a single method.
Summary
We started with an example of a React component that had three main tasks and a number of responsibilities behind them.
We extracted the "github" service from the component and the service can now be reused in other parts of the application.
We unburdened the component from the responsibility of fetching and processing the data from GitHub. The component's code is now a bit simpler.
Coming up next
In the second part of this series, we will focus on further refactoring of the extracted service and we will return to the main component in part three. Stay tuned!
Single Responsibility Principle in React Application series