Tiny components: what can go wrong? We use the principle of sole responsibility

Original author: Scott Domes
  • Transfer
We present to your attention the translation of the article Scott Domes, which was published on blog.bitsrc.io. Find out under the cut why components should be as small as possible and how the principle of sole responsibility affects the quality of applications.


Photo of Austin Kirk with Unsplash

The advantage of the React component system (and similar libraries) is that your UI is divided into small pieces that are easily perceived and can be reused.

These components are compact (100–200 lines), which allows other developers to easily understand and modify them.

Although the components, as a rule, try to make shorter, clear, there is no strict limitation of their length. React will not mind if you decide to fit your application into one frighteningly huge component consisting of 3,000 lines.

... but not worth it. Most of your components are likely to be too voluminous - or rather, they perform too many functions.

In this article I will prove that most of the components (even with the usual length of 200 lines) should be more focused. They must perform only one function, and perform it well. Eddie Osmani is wonderful about this here .

Tip : when working in JS, use Bit to organize, assemble and reuse components, like Lego details. Bit is an extremely effective tool for this business, it will help you and your team save time and speed up the build. Just try it.

Let's demonstrate exactly how something can go wrong when creating components .

Our application


Imagine that we have a standard application for bloggers. And that's what's on the main screen:

classMainextendsReact.Component{
  render() {
    return (
      <div><header>
          // Header JSX
        </header><asideid="header">
          // Sidebar JSX
        </aside><divid="post-container">
          {this.state.posts.map(post => {
            return (
              <divclassName="post">
                // Post JSX
              </div>
            );
          })}
        </div></div>
    );
  }
}

(This example, like many subsequent ones, should be viewed as pseudocode.)

This shows the top panel, side panel, and list of posts. It's simple.

Since we also need to upload posts, we can do this while the component is being mounted:

classMainextendsReact.Component{
  state = { posts: [] };
  componentDidMount() {
    this.loadPosts();
  }
  loadPosts() {
    // Load posts and save to state
  }
  render() {
    // Render code
  }
}

We also have some logic by which the sidebar is called. If the user clicks on the button in the top panel, the sidebar leaves. It can be closed both from the top and from the actual side panel.

classMainextendsReact.Component{
  state = { posts: [], isSidebarOpen: false };
  componentDidMount() {
    this.loadPosts();
  }
  loadPosts() {
    // Load posts and save to state
  }
  handleOpenSidebar() {
    // Open sidebar by changing state
  }
  handleCloseSidebar() {
    // Close sidebar by changing state
  }
  render() {
    // Render code
  }
}

Our component has become a little more difficult, but still easy to perceive.

It can be argued that all its parts serve one purpose: to display the main page of the application. So we follow the principle of sole responsibility.

The sole responsibility principle states that one component should perform only one function. If we reformulate the definition taken from wikipedia.org , it turns out that each component should be responsible only for one part of the [application] functional.

Our Main component meets this requirement. What is the problem?

This is another formulation of the principle: any [component] should have only one reason for change .

This definition is taken from a book by Robert Martin.“Rapid software development. Principles, examples, practice ” , and it is of great importance.

By focusing on one reason for changing our components, we can create better applications that, moreover, will be easy to configure.

For clarity, let's complicate our component.

Complication


Suppose that a month after the Main component was implemented, a developer from our team was assigned a new feature. Now the user will be able to hide any post (for example, if it contains inappropriate content).

It is not difficult to do!

classMainextendsReact.Component{
  state = { posts: [], isSidebarOpen: false, postsToHide: [] };
  // older methods
  get filteredPosts() {
    // Return posts in state, without the postsToHide
  }
  render() {
    return (
      <div><header>
          // Header JSX
        </header><asideid="header">
          // Sidebar JSX
        </aside><divid="post-container">
          {this.filteredPosts.map(post => {
            return (
              <divclassName="post">
                // Post JSX
              </div>
            );
          })}
        </div></div>
    );
  }
}

Our colleague coped with it easily. She just added one new method and one new property. No one of those who viewed the short list of changes had any objections.

A couple of weeks later, another feature is announced - an improved sidebar for the mobile version. Instead of messing with CSS, the developer decides to create several JSX components that will only run on mobile devices.

classMainextendsReact.Component{
  state = {
    posts: [],
    isSidebarOpen: false,
    postsToHide: [],
    isMobileSidebarOpen: false
  };
  // older methods
  handleOpenSidebar() {
    if (this.isMobile()) {
      this.openMobileSidebar();
    } else {
      this.openSidebar();
    }
  }
  openSidebar() {
    // Open regular sidebar
  }
  openMobileSidebar() {
    // Open mobile sidebar
  }
  isMobile() {
    // Check if mobile device
  }
  render() {
    // Render method
  }
}

Another small change. A pair of new successfully named methods and a new property.

And here we have a problem. Mainstill performs only one function (rendering the main screen), but you look at all these methods that we are dealing with now:

classMainextendsReact.Component{
  state = {
    posts: [],
    isSidebarOpen: false,
    postsToHide: [],
    isMobileSidebarOpen: false
  };
  componentDidMount() {
    this.loadPosts();
  }
  loadPosts() {
    // Load posts and save to state
  }
  handleOpenSidebar() {
    // Check if mobile then open relevant sidebar
  }
  handleCloseSidebar() {
    // Close both sidebars
  }
  openSidebar() {
    // Open regular sidebar
  }
  openMobileSidebar() {
    // Open mobile sidebar
  }
  isMobile() {
    // Check if mobile device
  }
  get filteredPosts() {
    // Return posts in state, without the postsToHide
  }
  render() {
    // Render method
  }
}

Our component becomes large and cumbersome, it is difficult to understand. And with the expansion of the functional situation will only worsen.

What went wrong?

The only reason


Let's go back to the definition of the principle of sole responsibility: any component should have only one reason for change .

Previously, we changed the way posts were displayed, so we had to change our Main component. Next, we changed the way the sidebar was opened - and again changed the Main component.

This component has many unrelated reasons for change. This means that it performs too many functions .

In other words, if you can significantly change part of your component and it does not lead to changes in another part of it, then your component has too much responsibility.

More efficient separation


The solution to the problem is simple: it is necessary to split the Main component into several parts. How to do it?

Start over. Rendering the main screen remains the responsibility of the Main component, but we reduce it only to display the related components:

classMainextendsReact.Component{
  render() {
    return (
      <Layout><PostList /></Layout>
    );
  }
}

Wonderful.

If we suddenly change the layout of the main screen (for example, add additional sections), then Main will change. In other cases, we will have no reason to touch it. Perfectly.

Let's go to Layout:

classLayoutextendsReact.Component{
  render() {
    return (
      <SidebarDisplay>
        {(isSidebarOpen, toggleSidebar) => (
          <div>
            <Header openSidebar={toggleSidebar} />
            <Sidebar isOpen={isSidebarOpen} close={toggleSidebar} />
          </div>
        )}
      </SidebarDisplay>
    );
  }
}

It's a little more complicated here. It Layoutis responsible for rendering the markup components (side panel / top panel). But we will not be tempted and we will not be given the Layoutresponsibility to determine whether the sidebar is open or not.

We assign this function to a component SidebarDisplaythat transfers the necessary methods or state to the components Headerand Sidebar.

(The above is an example of the Render Props via Children pattern in React. If you are not familiar with it, do not worry. It is important that there is a separate component that controls the open / closed state of the sidebar.)

And then, it Sidebarcan be quite simple if it answers just for rendering the sidebar on the right.

classSidebarextendsReact.Component{
  isMobile() {
    // Check if mobile
  }
  render() {
    if (this.isMobile()) {
      return<MobileSidebar />;
    } else {
      return <DesktopSidebar />;
    }
  }
}

Again, we resist the temptation to insert JSX for computers / mobile devices directly into this component, because in this case it will have two reasons for change.

Let's look at another component:

classPostListextendsReact.Component{
  state = { postsToHide: [] }
  filterPosts(posts) {
    // Show posts, minus hidden ones
  }
  hidePost(post) {
    // Save hidden post to state
  }
  render() {
    return (
      <PostLoader>
        {
          posts => this.filterPosts(posts).map(post => <Post />)
        }
      </PostLoader>
    )
  }
}

PostListchanges only if we change the way the list of posts is rendered. Seems obvious, right? This is exactly what we need.

PostLoaderchanges only if we change the way posts are loaded. And finally, it Postchanges only if we change the way the post is drawn.

Conclusion


All of these components are tiny and perform one small function. The reasons for the changes in them are easy to identify, and the components themselves are tested and corrected.

Now our application is much easier to modify - rearrange the components, add a new one, and extend the existing functionality. You just need to look at any component file to determine what it is for.

We know that our components will change and grow over time, but the application of this universal rule will help you to avoid technical debt and increase the speed of the team. You decide how to distribute the components, but remember - there must be only one reason to change a component .

Thank you for your attention, and look forward to your comments!

Also popular now: