What's inside
This is the second part of my blog post series about dealing with React components that have too many responsibilities. Have a look at the first part of the series (Single Responsibility Principle in React Applications – Part 1), if you haven't already.
Refactoring the data fetching service
The data fetching functionality is now encapsulated within the "github" service. That’s what we wanted to achieve from the perspective of the rest of our application ("client" code).
If fetching the latest pull request from React repo was the only task of the "github" service, we could just leave it in its current form - even though it has more than one responsibility.
However, in a real-life application it’s more likely that we would also fetch other kinds of information from GitHub (for example, a list of open issues) so we would like to reuse some of the functionality.
In the first part of the series, we left the service in the following form:
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,
}
});
}
});
}
We can see that the fetchLatestReactPullRequest function performs two main tasks:
1. Handling request - response operation,
2. Extracting the required data from the response.
Its single responsibility should be the following: "return the result of running the two tasks above, one after the other". Yet, right now it has more responsibilities because it also needs to know the details of how to perform each of these tasks.
Let's extract the request - response handler into fetchData action:
src/services/github/actions/fetchData/fetchData.js
export default function fetchData({ path }) {
return fetch(\`https://api.github.com/${path}\`, {
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;
}
})
.then(function(response) {
return response.json();
});
}
src/services/github/actions/fetchData/index.js
export { default } from './fetchData';
We can stop refactoring this piece of code here or break this functionality further.
We perform three steps here:
- Sending the request,
- Converting all non-200 responses to an error,
- Converting the successful response to JSON.
Let's extract each step to a separate function:
- sendRequest:
src/services/github/actions/fetchData/sendRequest.js
export default function sendRequest({ path }) {
return fetch(\`https://api.github.com/${path}\`, {
headers: {
'Accept': 'application/vnd.github.v3+json',
}
});
}
- throwErrorIfResponseCodeIsDifferentFrom200:
src/services/github/actions/fetchData/throwErrorIfResponseCodeIsDifferentFrom200.js
export default function throwErrorIfResponseCodeIsDifferentFrom200(response) {
if (response.status !== 200) {
throw new Error(\`Response status code: ${response.status}\`);
} else {
return response;
}
}
- mapResponseToJSON:
src/services/github/actions/fetchData/mapResponseToJSON.js
export default function mapResponseToJSON(response) {
return response.json();
}
And now fetchData becomes:
src/services/github/actions/fetchData/fetchData.js
import sendRequest from './sendRequest';
import throwErrorIfResponseCodeIsDifferentFrom200 from './throwErrorIfResponseCodeIsDifferentFrom200';
import mapResponseToJSON from './mapResponseToJSON';
export default function fetchData(requestData) {
return sendRequest(requestData)
.then(throwErrorIfResponseCodeIsDifferentFrom200)
.then(mapResponseToJSON);
}
Now the responsibility of fetchData is returning the result of the execution of these three steps. It’s no longer responsible for knowing how each step should be handled - that was just delegated to the newly created functions.
Another benefit of this refactoring is that now the content of fetchData reads like a table of contents we find in books: we only see chapter titles and details are deliberately hidden. If we want to see how an operation is performed, we can go to the specific function in the same way that we navigate from the table of contents to a specific chapter in a book.
Let's return to fetchLatestReactPullRequest, which after extracting fetchData becomes:
src/services/github/actions/fetchLatestReactPullRequest/fetchLatestReactPullRequest.js
import fetchData from '../fetchData';
export default function fetchLatestReactPullRequest() {
return fetchData({ path: 'repos/facebook/react/pulls?state=all&sort=created&direction=desc&per_page=1&page=1' })
.then(function([latestReactPullRequest]) {
return {
title: latestReactPullRequest.title,
body: latestReactPullRequest.body,
userLogin: latestReactPullRequest.user.login,
createdAt: latestReactPullRequest.created_at,
}
});
}
Although most of the details about how to communicate with GitHub API went into fetchData, we still need to deal with the path of the URL here. That bit couldn't go to fetchData because we want fetchData to be reusable for other GitHub API requests. Yet, we know that the path is an extra responsibility which we would like to delegate:
src/services/github/actions/fetchLatestReactPullRequest/buildFetchDataArgs.js
export default function buildFetchDataArgs() {
return { path: 'repos/facebook/react/pulls?state=all&sort=created&direction=desc&per_page=1&page=1' };
}
src/services/github/actions/fetchLatestReactPullRequest/fetchLatestReactPullRequest.js
import buildFetchDataArgs from './buildFetchDataArgs';
import fetchData from '../fetchData';
export default function fetchLatestReactPullRequest() {
return fetchData(buildFetchDataArgs())
.then(function([latestReactPullRequest]) {
return {
title: latestReactPullRequest.title,
body: latestReactPullRequest.body,
userLogin: latestReactPullRequest.user.login,
createdAt: latestReactPullRequest.created_at,
}
});
}
Finally, let's take data mapping out:
src/services/github/actions/fetchLatestReactPullRequest/extractDataFromResponseJSON.js
export default function extractDataFromResponseJSON([latestReactPullRequest]) {
return {
title: latestReactPullRequest.title,
body: latestReactPullRequest.body,
userLogin: latestReactPullRequest.user.login,
createdAt: latestReactPullRequest.created_at,
}
};
Similarly to fetchData, the single responsibility of fetchLatestReactPullRequest is now to glue the subroutines together. Details are delegated deeper.
src/services/github/actions/fetchLatestReactPullRequest/fetchLatestReactPullRequest.js
import buildFetchDataArgs from './buildFetchDataArgs';
import fetchData from '../fetchData';
import extractDataFromResponseJSON from './extractDataFromResponseJSON';
export default function fetchLatestReactPullRequest() {
return fetchData(buildFetchDataArgs())
.then(extractDataFromResponseJSON);
}
Allowing to specify the owner and repo
There is still one flaw that inhibits fetchLatestReactPullRequest action's code reuse: it has the GitHub's owner and repository names hardcoded. The same is true for the LatestReactPullRequest component: it can only show React's latest pull request. That is another responsibility. These pieces of code are responsible for knowing the owner and repo names.
Let's make the client code tell LatestReactPullRequest component what values to use.
Thus far, the component was taking no props:
<LatestReactPullRequest />
Let's add the possibility of specifying the props:
<LatestGitHubPullRequest owner="facebook" repo="react" />
Note that we are also changing the component name here. The old name indicated that the component was coupled with the React repository. The new name says that the component can handle any specified repository.
src/components/LatestGitHubPullRequest/LatestGitHubPullRequest.js
// ...
class LatestGitHubPullRequest extends Component {
// ...
componentDidMount() {
// ...
const { owner, repo } = this.props;
const fetchPromise = githubService.fetchLatestPullRequest({ owner, repo });
// ...
}
// ...
}
LatestGitHubPullRequest.propTypes = {
owner: PropTypes.string.isRequired,
repo: PropTypes.string.isRequired,
};
// …
src/services/github/actions/fetchLatestPullRequest/fetchLatestPullRequest.js
import buildFetchDataArgs from './buildFetchDataArgs';
import fetchData from '../fetchData';
import extractDataFromResponseJSON from './extractDataFromResponseJSON';
export default function fetchLatestPullRequest({ owner, repo }) {
return fetchData(buildFetchDataArgs({ owner, repo }))
.then(extractDataFromResponseJSON);
}
src/services/github/actions/fetchLatestPullRequest/buildFetchDataArgs.js
export default function buildFetchDataArgs({ owner, repo }) {
return { path: \`repos/${owner}/${repo}/pulls?state=all&sort=created&direction=desc&per_page=1&page=1\` };
}
As all the involved pieces of code are no longer tied to the specific React repo, we are also updating names, e.g. latestReactPullRequest to latestPullRequest:
src/services/github/actions/fetchLatestPullRequest/extractDataFromResponseJSON.js
export default function extractDataFromResponseJSON([latestPullRequest]) {
return {
title: latestPullRequest.title,
body: latestPullRequest.body,
userLogin: latestPullRequest.user.login,
createdAt: latestPullRequest.created_at,
}
};
Summary
We have refactored the internals of the "github" service into smaller pieces that can be reused internally when extending the service's functionality.
We have also decoupled the component from the hardcoded owner and repo names, so that it can handle any given GitHub repository.
One of the most important gains that we achieved here is reusability. These pieces of code are now more open to changes and adding new functionality.
What's next?
We are now leaving the "github" service. In the next post, we will return to the LatestGitHubPullRequest component to see how we can delegate its other responsibilities.
Single Responsibility Principle in React Application series