Single Responsibility Principle in React applications – Part 3

This is the third and the last part of a miniseries of blog posts. If you haven't done that already, read Part 1 and Part 2 of the series first. In Part 1, we started out with an example of a fat React component. We listed its tasks and extracted one of them, fetching data from GitHub, to a "github" service. In Part 2, we focused on refactoring internals of the "github" service that opened it for future functionality extensions. In this part of the series, we’re going back to the React component to delegate its other current responsibilities further.

Freeing the component from data fetching management

In Part 1, we left the LatestGitHubPullRequest component in the following state:
src/components/LatestGitHubPullRequest/LatestGitHubPullRequest.js
[cc lang="JavaScript" escaped="true" lines="100"] import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import moment from 'moment'; import makePromiseCancelable from '@utils/makePromiseCancelable'; import githubService from '@services/github'; import './LatestGitHubPullRequest.css'; const DATA_FETCHING_STATUS = {  NOT_STARTED: Symbol('DATA_FETCHING_STATUS_NOT_STARTED'),  IN_PROGRESS: Symbol('DATA_FETCHING_STATUS_IN_PROGRESS'),  SUCCESS:     Symbol('DATA_FETCHING_STATUS_SUCCESS'),  FAILURE:     Symbol('DATA_FETCHING_STATUS_FAILURE'), }; class LatestGitHubPullRequest 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 { owner, repo } = this.props;    const fetchPromise = githubService.fetchLatestPullRequest({ owner, repo });    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,          });      });  }  componentWillUnmount() {    this.cancelDataFetchingPromise();  }  render() {    const { dataFetchingStatus, data } = this.state;    return (      <div className="c-latest-github-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-github-pull-request__title">{data.title}</div>            <div className="c-latest-github-pull-request__body">{data.body}</div>            <div className="c-latest-github-pull-request__created-at-and-user-login">              {moment(data.createdAt).calendar()} by {data.userLogin}            </div>          </Fragment>        )}      </div>    );  } } LatestGitHubPullRequest.propTypes = {  owner: PropTypes.string.isRequired,  repo:  PropTypes.string.isRequired, }; export default LatestGitHubPullRequest; [/cc] Although the component doesn't need to know from where and how to handle data fetching, it still supervises the process. It calls the githubService.fetchLatestPullRequest(...) method, tracks the fetching status, and cancels the fetch promise if the component happens to be unmounted before the promise resolves. Note that the mentioned functionality belongs to a logically separate layer. It’s responsible for managing the data fetching process, while the rest of the component is responsible for displaying information. Let's reflect this in code. What if we extract the data fetching management code to a wrapper component which would then be passing the data and the status down to LatestGitHubPullRequest in props? A higher-order component (hoc) looks like a good candidate for the job:
src/hoc/withLatestGitHubPullRequest.js
[cc lang="JavaScript" escaped="true" lines="100"] import React, { Component } from 'react'; import PropTypes from 'prop-types'; import getComponentDisplayName from '@hoc/utils/getComponentDisplayName'; import makePromiseCancelable from '@utils/makePromiseCancelable'; import githubService from '@services/github'; import DATA_FETCHING_STATUS from '@consts/dataFetchingStatus'; export default function withLatestGitHubPullRequest(OriginalComponent) {  class WithLatestGitHubPullRequest 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 { owner, repo } = this.props;      const fetchPromise = githubService.fetchLatestPullRequest({ owner, repo });      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,            });        });    }    componentWillUnmount() {      this.cancelDataFetchingPromise();    }    render() {      const { owner, repo, ...otherProps } = this.props;      const { dataFetchingStatus, data } = this.state;      return (        <OriginalComponent          {...otherProps}          dataFetchingStatus={dataFetchingStatus}          data={data}        />      );    }  }  WithLatestGitHubPullRequest.displayName = `WithLatestGitHubPullRequest(${getComponentDisplayName(OriginalComponent)})`;  WithLatestGitHubPullRequest.propTypes = {    owner: PropTypes.string.isRequired,    repo:  PropTypes.string.isRequired,  };  return WithLatestGitHubPullRequest; } [/cc] The original component gets thinner:
src/components/GitHubPullRequest/GitHubPullRequest.js
[cc lang="JavaScript" escaped="true" lines="100"] import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import moment from 'moment'; import DATA_FETCHING_STATUS from '@consts/dataFetchingStatus'; import './GitHubPullRequest.css'; const GitHubPullRequest = ({ dataFetchingStatus, data }) => (  <div className="c-latest-github-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-github-pull-request__title">{data.title}</div>        <div className="c-latest-github-pull-request__body">{data.body}</div>        <div className="c-latest-github-pull-request__created-at-and-user-login">          {moment(data.createdAt).calendar()} by {data.userLogin}        </div>      </Fragment>    )}  </div> ); GitHubPullRequest.propTypes = {  dataFetchingStatus: PropTypes.oneOf(    Object.values(DATA_FETCHING_STATUS)  ).isRequired,  data: PropTypes.shape({    title:     PropTypes.string.isRequired,    body:      PropTypes.string.isRequired,    userLogin: PropTypes.string.isRequired,    createdAt: PropTypes.string.isRequired,  }), }; export default GitHubPullRequest; [/cc] See that we also removed the word Latest from the component's name, as now it’s no longer coupled specifically with the latest pull request. It can now display any pull request data provided via props. The component's reusability was improved. We moved DATA_FETCHING_STATUS to a separate file, as it’s now used in more than one place:
src/consts/dataFetchingStatus.js
[cc lang="JavaScript" escaped="true" lines="100"] export default {  NOT_STARTED: Symbol('DATA_FETCHING_STATUS_NOT_STARTED'),  IN_PROGRESS: Symbol('DATA_FETCHING_STATUS_IN_PROGRESS'),  SUCCESS:     Symbol('DATA_FETCHING_STATUS_SUCCESS'),  FAILURE:     Symbol('DATA_FETCHING_STATUS_FAILURE'), }; [/cc] And the original component is now wrapped with:
src/containers/LatestGitHubPullRequest/LatestGitHubPullRequest.js
[cc lang="JavaScript" escaped="true" lines="100"] import GitHubPullRequest from '@components/GitHubPullRequest'; import withLatestGitHubPullRequest from '@hoc/withLatestGitHubPullRequest'; export default withLatestGitHubPullRequest(GitHubPullRequest); [/cc] Let's focus for a moment on the extracted withLatestGitHubPullRequest hoc. If it's the only component in the application that fetches data and tracks the fetching status, we could leave it in its current shape. But let's say we have another component that fetches some other data. It can be a list of open React issues on GitHub, or any other information from any other service in the internet. Then both of these two components would probably contain the same status tracking code. We see that withLatestGitHubPullRequest fetching status tracking is coupled to a specific data fetch, namely: githubService.fetchLatestPullRequest(...). The other component would track the status in the same way. The only difference would be the function that it calls to get the data. Perhaps we could pass the data fetching function as an argument, thereby making the status tracking code independent from the actual data source?
src/hoc/withDataFetching.js
[cc lang="JavaScript" escaped="true" lines="100"] import React, { Component } from 'react'; import PropTypes from 'prop-types'; import getComponentDisplayName from '@hoc/utils/getComponentDisplayName'; import makePromiseCancelable from '@utils/makePromiseCancelable'; import DATA_FETCHING_STATUS from '@consts/dataFetchingStatus'; export default function withDataFetching(OriginalComponent) {  class WithDataFetching 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 { fetchData } = this.props;      const { promise, cancel } = makePromiseCancelable(fetchData());      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,            });        });    }    componentWillUnmount() {      this.cancelDataFetchingPromise();    }    render() {      const { fetchData, ...otherProps } = this.props;      const { dataFetchingStatus, data } = this.state;      return (        <OriginalComponent          {...otherProps}          dataFetchingStatus={dataFetchingStatus}          data={data}        />      );    }  }  WithDataFetching.displayName = `WithDataFetching(${getComponentDisplayName(OriginalComponent)})`;  WithDataFetching.propTypes = {    fetchData: PropTypes.func.isRequired,  };  return WithDataFetching; } [/cc] We extracted most of the code into withDataFetching hoc. It takes fetchData function via props. The contract is that the function returns a promise. And that's it. The withDataFetching doesn't know what data are fetched and from where. The only responsibility it has is tracking when the fetching starts and whether it succeeds or fails. That means withDataFetching hoc is now reusable as it can supervise fetching from any data source. Our withLatestGitHubPullRequest hoc is now responsible for building the fetchData function and passing it to the data fetching component.
src/hoc/withLatestGitHubPullRequest.js
[cc lang="JavaScript" escaped="true" lines="100"] import React from 'react'; import PropTypes from 'prop-types'; import getComponentDisplayName from '@hoc/utils/getComponentDisplayName'; import githubService from '@services/github'; export default function withLatestGitHubPullRequest(OriginalComponent) {  const WithLatestGitHubPullRequest = function({ owner, repo, ...otherProps }) {    const fetchData = function() {      return githubService.fetchLatestPullRequest({ owner, repo });    };    return (      <OriginalComponent        {...otherProps}        fetchData={fetchData}      />    );  }  WithLatestGitHubPullRequest.displayName = `WithLatestGitHubPullRequest(${getComponentDisplayName(OriginalComponent)})`;  WithLatestGitHubPullRequest.propTypes = {    owner: PropTypes.string.isRequired,    repo:  PropTypes.string.isRequired,  };  return WithLatestGitHubPullRequest; } [/cc] And now we put it all together:
src/containers/LatestGitHubPullRequest/LatestGitHubPullRequest.js
[cc lang="JavaScript" escaped="true" lines="100"] import GitHubPullRequest from '@components/GitHubPullRequest'; import withDataFetching from '@hoc/withDataFetching'; import withLatestGitHubPullRequest from '@hoc/withLatestGitHubPullRequest'; export default withLatestGitHubPullRequest(withDataFetching(GitHubPullRequest)); [/cc] Finally, let's return to the GitHubPullRequest component. It now has two responsibilities:
  1. displaying the fetching status, and
  2. displaying the pull request information.
Let's relieve the component from the first responsibility. We can do that by extracting the code to DataFetchingStatus component. Its only responsibility is displaying the status information:
src/components/DataFetchingStatus/DataFetchingStatus.js
[cc lang="JavaScript" escaped="true" lines="100"] import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import DATA_FETCHING_STATUS from '@consts/dataFetchingStatus'; import './DataFetchingStatus.css'; const DataFetchingStatus = ({ dataFetchingStatus, data }) => (  <div className="c-data-fetching-status">    {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>Data fetched successfully.</Fragment>    )}  </div> ); DataFetchingStatus.propTypes = {  dataFetchingStatus: PropTypes.oneOf(    Object.values(DATA_FETCHING_STATUS)  ).isRequired, }; export default DataFetchingStatus; [/cc] And GitHubPullRequest is now left with the responsibility to display the pull request information:
src/components/GitHubPullRequest/GitHubPullRequest.js
[cc lang="JavaScript" escaped="true" lines="100"] import React from 'react'; import PropTypes from 'prop-types'; import moment from 'moment'; import './GitHubPullRequest.css'; const GitHubPullRequest = ({ data: { title, body, userLogin, createdAt } }) => (  <div className="c-latest-github-pull-request">    <div className="c-latest-github-pull-request__title">{title}</div>    <div className="c-latest-github-pull-request__body">{body}</div>    <div className="c-latest-github-pull-request__created-at-and-user-login">      {moment(createdAt).calendar()} by {userLogin}    </div>  </div> ); GitHubPullRequest.propTypes = {  data: PropTypes.shape({    title:     PropTypes.string.isRequired,    body:      PropTypes.string.isRequired,    userLogin: PropTypes.string.isRequired,    createdAt: PropTypes.string.isRequired,  }), }; export default GitHubPullRequest; [/cc] We could  extract the creation date formatting functionality even further, but for the purpose of this article we will stop here. How do we make DataFetchingStatus and GitHubPullRequest work together? Let's use another hoc.
src/hoc/withDataFetchingStatus.js
[cc lang="JavaScript" escaped="true" lines="100"] import React from 'react'; import PropTypes from 'prop-types'; import getComponentDisplayName from '@hoc/utils/getComponentDisplayName'; import DATA_FETCHING_STATUS from '@consts/dataFetchingStatus'; export default function withDataFetchingStatus(DataFetchingStatus) {  return function(OriginalComponent) {    const WithDataFetchingStatus = function({ dataFetchingStatus, ...otherProps }) {      return (        (dataFetchingStatus === DATA_FETCHING_STATUS.SUCCESS          ? <OriginalComponent {...otherProps} />          : <DataFetchingStatus dataFetchingStatus={dataFetchingStatus} />        )      );    }    WithDataFetchingStatus.displayName = `WithDataFetchingStatus(${getComponentDisplayName(OriginalComponent)})`;    WithDataFetchingStatus.propTypes = {      dataFetchingStatus: PropTypes.oneOf(        Object.values(DATA_FETCHING_STATUS)      ).isRequired,    };    return WithDataFetchingStatus;  }; } [/cc]
src/containers/LatestGitHubPullRequest/LatestGitHubPullRequest.js
[cc lang="JavaScript" escaped="true" lines="100"] import GitHubPullRequest from '@components/GitHubPullRequest'; import DataFetchingStatus from '@components/DataFetchingStatus'; import withDataFetching from '@hoc/withDataFetching'; import withDataFetchingStatus from '@hoc/withDataFetchingStatus'; import withLatestGitHubPullRequest from '@hoc/withLatestGitHubPullRequest'; export default withLatestGitHubPullRequest(  withDataFetching(    withDataFetchingStatus(DataFetchingStatus)(      GitHubPullRequest    )  ) ); [/cc] In this particular setup, the DataFetchingStatus won't have opportunity to render the information about success, because itself it’s not rendered in such scenario; the actual data is. Yet we've given DataFetchingStatus this functionality to make it a fully autonomous component, handling all the statuses. That concludes the refactoring.

Summary

In this post, we split the React component that we started with in Part 1 further. We extracted separate parts responsible for:
  • deciding which data fetching functionality to use (withLatestGitHubPullRequest hoc),
  • managing the fetching process (withDataFetching hoc),
  • displaying the fetching status (DataFetchingStatus component),
  • displaying the pull request data (GitHubPullRequest component),
  • deciding whether to display the status or the data (withDataFetchingStatus hoc).
Along with previously extracted "github" service, these are now building blocks that comprise the functionality to display the latest React repo pull request. Note that the total number of code lines is higher now than it was at the beginning. The main reasons behind that are the extra code that we now need to glue the parts together and the language syntax overhead. Yet, the code blocks themselves are smaller and simpler. There are fewer execution paths inside each of them. And it's usually easier to understand the system when analysing one part at a time rather than everything at once. Once the original fat React component was decomposed into parts, these parts could now be reused to extend the existing functionality with a minimal amount of new code required. Having single-responsibility building blocks at our disposal, we can now easily select those that offer what is necessary for a particular new feature; none of them carry the baggage of unwanted functionality. Encapsulating each responsibility in a separate function, class, or service gives us valuable separation of concerns. It's far easier to find a piece where a specific functionality is implemented, isolate buggy code, or simply replace an existing element with a new one. The code is much more testable as well. A smaller number of code execution paths means there are fewer scenarios to test. Each part can be unit-tested separately so that we know our building blocks work properly. All this makes the software more reliable. Any code changes and extensions are cheaper and quicker.

Conclusion

The three parts of this series showed us that React components don’t need to be fat. We just have to be aware of the responsibilities each piece of code has and then consciously decide whether we want to delegate them. The Single Responsibility Principle can be applied to brand-new React components as well as to the existing fat ones through refactoring. Have you got any questions about applying the Single Responsibility Principle in React applications? Reach out to me in comments and stay tuned for more useful content we plan to share on our blog.

Single Responsibility Principle in React applications – Part 2

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, 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
[cc lang="JavaScript" escaped="true" lines="100"] 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,             }           });       }     }); } [/cc] 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
[cc lang="JavaScript" escaped="true" lines="100"] 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();     }); } [/cc]
src/services/github/actions/fetchData/index.js
[cc lang="JavaScript" escaped="true" lines="100"] export { default } from './fetchData'; [/cc] 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
[cc lang="JavaScript" escaped="true" lines="100"] export default function sendRequest({ path }) {   return fetch(`https://api.github.com/${path}`, {     headers: {       'Accept': 'application/vnd.github.v3+json',     }   }); } [/cc]
  • throwErrorIfResponseCodeIsDifferentFrom200:
src/services/github/actions/fetchData/throwErrorIfResponseCodeIsDifferentFrom200.js
[cc lang="JavaScript" escaped="true" lines="100"] export default function throwErrorIfResponseCodeIsDifferentFrom200(response) {   if (response.status !== 200) {     throw new Error(`Response status code: ${response.status}`);   } else {     return response;   } } [/cc]
  • mapResponseToJSON:
src/services/github/actions/fetchData/mapResponseToJSON.js
[cc lang="JavaScript" escaped="true" lines="100"] export default function mapResponseToJSON(response) {   return response.json(); } [/cc] And now fetchData becomes:
src/services/github/actions/fetchData/fetchData.js
[cc lang="JavaScript" escaped="true" lines="100"] import sendRequest from './sendRequest'; import throwErrorIfResponseCodeIsDifferentFrom200 from './throwErrorIfResponseCodeIsDifferentFrom200'; import mapResponseToJSON from './mapResponseToJSON'; export default function fetchData(requestData) {   return sendRequest(requestData)     .then(throwErrorIfResponseCodeIsDifferentFrom200)     .then(mapResponseToJSON); } [/cc] 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
[cc lang="JavaScript" escaped="true" lines="100"] 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,       }     }); } [/cc] 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
[cc lang="JavaScript" escaped="true" lines="100"] export default function buildFetchDataArgs() {   return { path: 'repos/facebook/react/pulls?state=all&sort=created&direction=desc&per_page=1&page=1' }; } [/cc]
src/services/github/actions/fetchLatestReactPullRequest/fetchLatestReactPullRequest.js
[cc lang="JavaScript" escaped="true" lines="100"] 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,       }     }); } [/cc] Finally, let's take data mapping out:
src/services/github/actions/fetchLatestReactPullRequest/extractDataFromResponseJSON.js
[cc lang="JavaScript" escaped="true" lines="100"] export default function extractDataFromResponseJSON([latestReactPullRequest]) {   return {     title:     latestReactPullRequest.title,     body:      latestReactPullRequest.body,     userLogin: latestReactPullRequest.user.login,     createdAt: latestReactPullRequest.created_at,   } }; [/cc] 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
[cc lang="JavaScript" escaped="true" lines="100"] import buildFetchDataArgs from './buildFetchDataArgs'; import fetchData from '../fetchData'; import extractDataFromResponseJSON from './extractDataFromResponseJSON'; export default function fetchLatestReactPullRequest() {   return fetchData(buildFetchDataArgs())     .then(extractDataFromResponseJSON); } [/cc]  

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: [cc lang="JavaScript" escaped="true" lines="100"] <LatestReactPullRequest /> [/cc] Let's add the possibility of specifying the props: [cc lang="JavaScript" escaped="true" lines="100"] <LatestGitHubPullRequest owner="facebook" repo="react" /> [/cc] 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
[cc lang="JavaScript" escaped="true" lines="100"] // ... 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, }; // … [/cc]
src/services/github/actions/fetchLatestPullRequest/fetchLatestPullRequest.js
[cc lang="JavaScript" escaped="true" lines="100"] 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); } [/cc]
src/services/github/actions/fetchLatestPullRequest/buildFetchDataArgs.js
[cc lang="JavaScript" escaped="true" lines="100"] export default function buildFetchDataArgs({ owner, repo }) {   return { path: `repos/${owner}/${repo}/pulls?state=all&sort=created&direction=desc&per_page=1&page=1` }; } [/cc] 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
[cc lang="JavaScript" escaped="true" lines="100"] export default function extractDataFromResponseJSON([latestPullRequest]) {   return {     title:     latestPullRequest.title,     body:      latestPullRequest.body,     userLogin: latestPullRequest.user.login,     createdAt: latestPullRequest.created_at,   } }; [/cc]  

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 applications – Part 1

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
[cc lang="JavaScript" escaped="true" lines="100"] 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; [/cc]  

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
[cc lang="JavaScript" escaped="true" lines="100"] // ...   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,         });       });   } // ... [/cc]   Since the code in stage 1 is now component-independent, we can extract it to a service.
src/services/github/actions/fetchLatestReactPullRequest/fetchLatestReactPullRequest.js
[cc lang="JavaScript" escaped="true" lines="100"] 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,             }           });       }     });   } [/cc]
src/services/github/actions/fetchLatestReactPullRequest/index.js
[cc lang="JavaScript" escaped="true"] export { default } from './fetchLatestReactPullRequest'; [/cc]
src/services/github/index.js
[cc lang="JavaScript" escaped="true"] import fetchLatestReactPullRequest from './actions/fetchLatestReactPullRequest'; export default {   fetchLatestReactPullRequest, }; [/cc]
src/components/LatestReactPullRequest/LatestReactPullRequest.js
[cc lang="JavaScript" escaped="true" lines="100"] // ... import githubService from '@services/github'; // ...   componentDidMount() {     // ...     // stage 1     const fetchPromise = githubService.fetchLatestReactPullRequest();     // stage 2     // ...   } // ... [/cc] 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!

Join our newsletter.