React and React Native
上QQ阅读APP看书,第一时间看更新

Refactoring component structures

We have a monolithic feature component—now what? Let's make it better.

In this section, you'll learn how to take the feature component that we just implemented in the preceding section and split it into more maintainable components. We'll start with the JSX, as this is probably the best refactor starting point. Then, we'll implement new components for the feature.

Finally, we'll make these new components functional, instead of class-based.

Start with the JSX

The JSX of any monolithic component is the best starting point for figuring out how to refactor it into smaller components. Let's visualize the structure of the component that we're currently refactoring:

The top part of the JSX is form controls, so this could easily become its own component:

<header> 
  <h1>Articles</h1> 
  <input 
    placeholder="Title" 
    value={title} 
    onChange={this.onChangeTitle} 
  /> 
  <input 
    placeholder="Summary" 
    value={summary} 
    onChange={this.onChangeSummary} 
  /> 
  <button onClick={this.onClickAdd}>Add</button> 
</header> 

Next, we have the list of articles:

<ul> 
  {articles.map(i => ( 
    <li key={i.id}> 
      <a 
        href="#" 
         
        onClick={ 
          this.onClickToggle.bind(null, i.id) 
        } 
      > 
        {i.title} 
      </a> 
      &nbsp; 
      <a 
        href="#" 
         
        onClick={this.onClickRemove.bind(null, i.id)} 
      > 
        ✗
      </a> 
      <p style={{ display: i.display }}> 
        {i.summary} 
      </p> 
    </li> 
  ))} 
</ul> 

Within this list, we have the potential for an article item, which would be everything in the <li> tag.

As you can see, the JSX alone paints a picture of how the UI structure can be decomposed into smaller React components. This refactoring exercise would be difficult without declarative JSX markup.

Implementing an article list component

Here's what the article list component implementation looks like:

import React, { Component } from 'react'; 
 
export default class ArticleList extends Component { 
  render() { 
    // The properties include things that are passed in 
    // from the feature component. This includes the list 
    // of articles to render, and the two event handlers 
    // that change state of the feature component. 
    const { 
      articles, 
      onClickToggle, 
      onClickRemove, 
    } = this.props; 
 
    return ( 
      <ul> 
        {articles.map(i => ( 
          <li key={i.id}> 
            { /* The "onClickToggle()" callback changes 
                 the state of the "MyFeature" component. */ } 
            <a 
              href="#" 
               
              onClick={onClickToggle.bind(null, i.id)} 
            > 
              {i.title} 
            </a> 
            &nbsp; 
 
            { /* The "onClickRemove()" callback changes 
                 the state of the "MyFeature" component. */ } 
            <a 
              href="#" 
               
              onClick={onClickRemove.bind(null, i.id)} 
            > 
              ✗ 
            </a> 
            <p style={{ display: i.display }}> 
              {i.summary} 
            </p> 
          </li> 
        ))} 
      </ul> 
    ); 
  } 
} 

As you can see, we're just taking the relevant JSX out of the monolithic component and putting it here. Now let's see what the feature component JSX looks like:

render() { 
  const { 
    articles, 
    title, 
    summary, 
  } = this.data.toJS(); 
 
  return ( 
    <section> 
      <header> 
        <h1>Articles</h1> 
        <input 
          placeholder="Title" 
          value={title} 
          onChange={this.onChangeTitle} 
        /> 
        <input 
          placeholder="Summary" 
          value={summary} 
          onChange={this.onChangeSummary} 
        /> 
        <button onClick={this.onClickAdd}>Add</button> 
      </header> 
 
      { /* Now the list of articles is rendered by the 
           "ArticleList" component. This component can 
           now be used in several other components. */ } 
      <ArticleList 
        articles={articles} 
        onClickToggle={this.onClickToggle} 
        onClickRemove={this.onClickRemove} 
      /> 
    </section> 
  ); 
} 

The list of articles is now rendered by the <ArticleList> component. The list of articles to render is passed to this component as a property as well as two of the event handlers.

Note

Wait, why are we passing event handlers to a child component? The reason is simple; it is so that the ArticleList component doesn't have to worry about state or how the state changes. All it cares about is rendering content, and making sure the appropriate event callbacks are hooked up to the appropriate DOM elements. This is a container component concept that I'll expand upon later in this chapter.

Implementing an article item component

After implementing the article list component, you might decide that it's a good idea to break this component down further, because the item might be rendered in another list on another page. Perhaps, the most important aspect of implementing the article list item as its own component is that we don't know how the markup will change in the future.

Another way to look at it is this—if it turns out that we don't actually need the item as its own component, this new component doesn't introduce much indirection or complexity. Without further ado, here's the article item component:

import React, { Component } from 'react'; 
 
export default class ArticleItem extends Component { 
  render() { 
    // The "article" is mapped from the "ArticleList" 
    // component. The "onClickToggle()" and 
    // "onClickRemove()" event handlers are passed 
    // all the way down from the "MyFeature" component. 
    const { 
      article, 
      onClickToggle, 
      onClickRemove, 
    } = this.props; 
 
    return ( 
      <li> 
        { /* The "onClickToggle()" callback changes 
             the state of the "MyFeature" component. */ } 
        <a 
          href="#" 
           
          onClick={onClickToggle.bind(null, article.id)} 
        > 
          {article.title} 
        </a> 
        &nbsp; 
 
        { /* The "onClickRemove()" callback changes 
             the state of the "MyFeature" component. */ } 
        <a 
          href="#" 
           
          onClick={onClickRemove.bind(null, article.id)} 
        > 
          ✗ 
        </a> 
        <p style={{ display: article.display }}> 
          {article.summary} 
        </p> 
      </li> 
    ); 
  } 
} 

Here's the new ArticleItem component being rendered by the ArticleList component:

import React, { Component } from 'react'; 
import ArticleItem from './ArticleItem'; 
 
export default class ArticleList extends Component { 
  render() { 
    // The properties include things that are passed in 
    // from the feature component. This includes the list 
    // of articles to render, and the two event handlers 
    // that change state of the feature component. These, 
    // in turn, are passed to the "ArticleItem" component. 
    const { 
      articles, 
      onClickToggle, 
      onClickRemove, 
    } = this.props; 
 
    // Now this component maps to an "<ArticleItem>"  
    // collection. 
    return ( 
      <ul> 
        {articles.map(i => ( 
          <ArticleItem 
            key={i.id} 
            article={i} 
            onClickToggle={onClickToggle} 
            onClickRemove={onClickRemove} 
          /> 
        ))} 
      </ul> 
    ); 
  } 
} 

Do you see how this list just maps the list of articles? What if we wanted to implement another article list that does some filtering too? It's beneficial to have a reusable ArticleItem component.

Implementing an add article component

Now that we're done with the article list, it's time to think about the form controls used to add a new article. Let's implement a component for this aspect of the feature:

import React, { Component } from 'react'; 
 
export default class AddArticle extends Component{ 
  render() { 
    const { 
      name, 
      title, 
      summary, 
      onChangeTitle, 
      onChangeSummary, 
      onClickAdd 
    } = this.props; 
 
    return ( 
      <section> 
        <h1>{name}</h1> 
        <input 
          placeholder="Title" 
          value={title} 
          onChange={onChangeTitle} 
        /> 
        <input 
          placeholder="Summary" 
          value={summary} 
          onChange={onChangeSummary} 
        /> 
        <button onClick={onClickAdd}>Add</button> 
      </section> 
    ); 
  } 
} 

Now, we have the final version of the feature component JSX:

render() { 
  const {  
    articles,  
    title,  
    summary, 
  } = this.state.data.toJS(); 
 
  return ( 
    <section> 
      { /* Now the add article form is rendered by the 
           "AddArticle" component. This component can 
           now be used in several other components. */ } 
      <AddArticle 
        name="Articles" 
        title={title} 
        summary={summary} 
        onChangeTitle={this.onChangeTitle} 
        onChangeSummary={this.onChangeSummary} 
        onClickAdd={this.onClickAdd} 
      /> 
 
      { /* Now the list of articles is rendered by the 
           "ArticleList" component. This component can 
           now be used in several other components. */ } 
      <ArticleList 
        articles={articles} 
        onClickToggle={this.onClickToggle} 
        onClickRemove={this.onClickRemove} 
      /> 
    </section> 
  ); 
} 

As you can see, the focus of this component is on the feature data while it defers to other components for rendering UI elements. Let's make one final tweak to the new components we've implemented for this feature.

Making components functional

While implementing these new components for the feature, you might have noticed that they don't have any responsibilities other than rendering JSX using property values. These components are good candidates for pure function components. Whenever you come across components that only use property values, it's a good idea to make them functional. For one thing, it makes it explicit that the component doesn't rely on any state or lifecycle methods. It's also more efficient, because React doesn't perform as much work when it detects that a component is a function.

Here is the functional version of the article list component:

import React from 'react'; 
import ArticleItem from './ArticleItem'; 
 
export default ({ 
  articles, 
  onClickToggle, 
  onClickRemove, 
}) => ( 
  <ul> 
    {articles.map(i => ( 
      <ArticleItem 
        key={i.id} 
        article={i} 
        onClickToggle={onClickToggle} 
        onClickRemove={onClickRemove} 
      /> 
    ))} 
  </ul> 
); 

Here is the functional version of the article item component:

import React from 'react'; 
 
export default ({ 
  article, 
  onClickToggle, 
  onClickRemove, 
}) => ( 
  <li> 
    { /* The "onClickToggle()" callback changes 
         the state of the "MyFeature" component. */ } 
    <a 
      href="#" 
       
      onClick={onClickToggle.bind(null, article.id)} 
    > 
      {article.title} 
    </a> 
    &nbsp; 
 
    { /* The "onClickRemove()" callback changes 
         the state of the "MyFeature" component. */ } 
    <a 
      href="#" 
       
      onClick={onClickRemove.bind(null, article.id)} 
    > 
      ✗ 
    </a> 
    <p style={{ display: article.display }}> 
      {article.summary} 
    </p> 
  </li> 
); 

Here is the functional version of the add article component:

import React from 'react'; 
 
export default ({ 
  name, 
  title, 
  summary, 
  onChangeTitle, 
  onChangeSummary, 
  onClickAdd, 
}) => ( 
  <section> 
    <h1>{name}</h1> 
    <input 
      placeholder="Title" 
      value={title} 
      onChange={onChangeTitle} 
    /> 
    <input 
      placeholder="Summary" 
      value={summary} 
      onChange={onChangeSummary} 
    /> 
    <button onClick={onClickAdd}>Add</button> 
  </section> 
); 

Another added benefit of making components functional is that there's less opportunity to introduce unnecessary methods or other data, because it's not a class where it's easier to add more stuff.