Don't write the same thing: how reused React components will help front-end developers create applications faster


Making the same changes in three or four different places in the JS code is an art that requires attention. If there are more elements, code support turns into flour. Therefore, for long-term or large projects, you should write code so that it can be carried out in separate components.

I’ve been engaged in front-end development for 10 years and will talk about the use of components to create front-end elements - this greatly simplifies the life of a front-end developer.

Written with support from Mail.ru Cloud Solutions .

What are frontend components and why are they needed


HTML tags - conditionally "zero" level of components. Each of them has its own functions and purpose.

CSS classes are the next level of abstraction that is usually reached when creating even a small site. In the rules for applying styles to a CSS class, we describe the behavior of all elements that are part of a conditional subset of elements.

The rules applied to CSS classes, as well as any other elements, such as HTML tags, allow you to centrally set and change the rules for displaying any number of elements of the same type. There are various tools for working with element styles - actually CSS, Sass, LESS, PostCSS, and methodologies for applying styles - BEM, SMACSS, Atomic CSS, CSS Modules, Styled components.

Actually components are:

  • elements of the same type that have both the same styles and the same layout (HTML) and behavior (JS);
  • similar in style and behavior elements that differ slightly from each other.

Web Components technology is being developed, which allows you to create custom HTML tags and include template pieces of code in the layout. However, components have become widely used thanks to modern front-end development frameworks such as Angular, Vue, React. JavaScript features make it easy to connect a component:

import {Header, Footer} from "./components/common";
render() {
    return (
       ...
   )
}

All major projects come to their library of ready-made components or to use one of the ready-made ones. The question of when you need to move from copying code to creating components is decided individually, there are no unambiguous recipes.

It is worth remembering not only the writing of the code, but also its support. A simple copy / paste of the same layout and isolating styles in CSS classes can create a display for some time without any particular risks. But if a behavior logic written in JS is added to each element, the benefits of reusing the code are felt literally from 2-3 elements, especially when it comes to supporting and modifying previously written code.

Reusable React Components


Suppose our application has become large enough, and we decided to write our own library of components. I suggest using the popular React front-end development tool for this. One of its advantages is the ability to simply and efficiently use embedded components. In the code below, the older component of the App uses three nested components: AppHeader, Article, AppFooter:

import React from "react";
import AppHeader from "./components/AppHeader";
import Article from "./components/Article";
import AppFooter from "./components/AppFooter";
export default class App extends React.Component {
    constructor(props) {
        super(props); 
        this.state = {
            title : "My App",
            contacts : "8 800 100 20 30"
           firtsArticleTitle : "Welcome",
           secondArticleTitle : "Let's speak about..."
        }
    };

    render() {
        return (
            <>
                <AppHeader 
                title={this.state.title}
            />
            <Article
                   title={this.state.firstArticleTitle}
               />
               <Article
                   title={this.state.secondArticleTitle}
               />               
               <AppFooter
                   contacts={this.state.contacts}
               />
           </>
       )
   }
}

Please note : now it is not required to use the senior wrapping tag in the layout - usually it was div. Modern React offers the Fragment tool, an abbreviated record of which <></>. Within these tags, you can use a flat tag hierarchy, as in the example above.

We used three library components, one of which is two times in one block. Data from the parent application is transferred to the props of the component and will be available inside it through the property this.props. This approach is typical for React and allows you to quickly assemble a view from typical elements. Especially if your application has many similar pages that differ only in the content of the articles (Model) and functionality.

However, we may need to modify the library component. For example, components with the same functionality may differ not only in textual content, but also in design: color, indents, borders. It is also possible to provide different functionalities of the same component.

The following case is considered below: depending on the presence of a callback, our component may be “responsive” or simply remain a View to render an element on the page:

// App.js
...
render() {
    return (        
        <Article 
            text={this.state.articleText}
            onClick={(e) => this.bindTap(e)}
           customClass={this.state.mainCustomClass}
        />                
    )
}

// Article.js
import React from "react";

export default class Article extends React.Component {
    constructor(props) {
        super(props);         
    };

    render() {
       let cName="default";
       if (this.props.customClass) cName = cName + " " this.props.customClass;
       let bgColor="#fff";
       if (this.props.bgColor) bgColor = this.props.bgColor;
        return (
            {this.props.onClick &&
            <div
                   className={cName}
                onClick={(e) => this.props.onClick(e)}
                   style={{background : bgColor}}
            >
                <p>{this.props.text}<p/>
            </div>
            }
            {!this.props.onClick && 
                <div className={cName}>
                <p>{this.props.text}<p/>
                </div>
           }
        )
    }
} 

In React, there is another technique for expanding the capabilities of components. In the call parameters, you can transfer not only data or callbacks, but also the whole layout:

// App.js
...
render() {
    return (        
        <Article 
            title={this.state.articleTitle}
            text={
               <>
                <p>Please read the article</p>
                <p>Thirst of all, I should say programming React is a very good practice.</p>
               </>
            }
        />                
    )
}

// Article.js
import React from "react";
export default class Article extends React.Component {
    constructor(props) {
        super(props);         
    };

    render() {
        return (
            <div className="article">
            <h2>{this.props.title}</h2>
            {this.props.text}
            </div>
        )
    }
}

The internal layout of the component will be reproduced in its entirety as it was transferred to props.

It is often more convenient to transfer additional layout to the library component using the insert and use pattern this.props.children. This approach is better for modifying the common components responsible for the typical blocks of an application or site where various internal content is assumed: caps, sidebars, blocks with ads, and others.

// App.js
...
render() {
    return (        
        <Article title={this.state.articleTitle}>
           <p>Please read the article</p>
            <p>First of all, I should say programming React is a very good practice.</p>
       </Article>                          
    )
}

// Article.js
import React from "react";
export default class Article extends React.Component {
    constructor(props) {
        super(props);         
    };

    render() {
        return (
            <div className="article">
            <h2>{this.props.title}</h2>
            {this.props.children}
            </div>
        )
    }
} 

Complete React Components


The components that are responsible only for View were considered above. However, we most likely will need to submit to the libraries not only the mapping, but also the standard data processing logic.

Let's look at the Phone component, which is designed to enter a phone number. It can mask the entered number using the plug-in validator library and inform the senior component that the phone is entered correctly or incorrectly:

// Phone.js
import React from "react";
import Validator from "../helpers/Validator";
export default class Phone extends React.Component {
    constructor(props) {
        super(props);   
        this.state = {
            value : this.props.value || "",
            name : this.props.name,
            onceValidated : false,
            isValid : false,
            isWrong : true
        }
        this.ref = React.createRef();    
    };

    componentDidMount = () => {
        this.setValidation();
    };

    setValidation = () => {
        const validationSuccess = (formattedValue) => {
            this.setState({
            value : formattedValue,
            isValid : true,
            isWrong : false,
            onceValidated : true
           });
            this.props.setPhoneValue({
            value : formattedValue, 
            item : this.state.name, 
            isValid : true
            })
        }
        const validationFail = (formattedValue) => {
            this.setState({
            value : formattedValue,
            isValid : false,
            isWrong : true,
            });
            this.props.setPhoneValue({
            value : formattedValue, 
            item : this.state.name, 
            isValid : false
            })
        }
        new Validator({
            element : this.ref.current,
            callbacks : {
            success : validationSuccess,
            fail : validationFail
            }
        });
    }

    render() {
        return (
            <div className="form-group">
            <labeL htmlFor={this.props.name}>
                    <input 
                name={this.props.name}
                id={this.props.name}
                type="tel"
                placeholder={this.props.placeholder}
                defaultValue={this.state.value}
                ref={this.ref}
                />
            </label>
            </div>
        )
    }

} 

This component already has an internal state state, a part of which it can share with the external code that called it. The other part remains inside the component, in the example above it onceValidated. Thus, part of the logic of the component is completely enclosed in it.

It can be said that typical behavior is independent of other parts of the application. For example, depending on whether the number was validated or not, we can display different text prompts. We took into a separate reusable component not only the display, but also the data processing logic.

MV components


If our standard component supports advanced functionality and has a sufficiently developed logic of behavior, then it is worth splitting it into two:

  • “Smart” for working with data ( Model);
  • "Dumb" to display ( View).

The connection will continue by calling one component. Now it will be Model. The second part - View- will be called in render()with props, some of which came from the application, and the other part is already state of the component itself:

// App.js
...
render() {
    return (        
        <Phone 
            name={this.state.mobilePhoneName}
            placeholder={"You mobile phone"}
        />                
    )
}

// Phone.js
import React from "react";
import Validator from "../helpers/Validator";
import PhoneView from "./PhoneView";
export default class Phone extends React.Component {
    constructor(props) {
        super(props);   
        this.state = {
            value : this.props.value || "",
            name : this.props.name,
            onceValidated : false,
            isValid : false,
            isWrong : true
        }
        this.ref = React.createRef();    
    };

    componentDidMount = () => {
        this.setValidation();
    };

    setValidation = () => {
        const validationSuccess = (formattedValue) => {
            ...
        }
        const validationFail = (formattedValue) => {
            ...
        }
        new Validator({
           element : this.ref.current,
            ...
        });
    }    

   render() {
        return (
            <PhoneView
                name={this.props.name}
            placeholder={this.props.placeholder}
               value={this.state.value}
               ref={this.ref}
            />
        )
    }
}

// PhoneView.js
import React from "react";
const PhoneView = React.forwardRef((props, ref) => (   
    <div className="form-group">
        <labeL htmlFor={props.name}>
            <input 
                name={props.name}
            id={props.name}
            type="tel"
            ref={ref}
            placeholder={props.placeholder}
            value={props.value}                
            />
        </label>
    </div>    
));
export default PhoneView;

It is worth paying attention to the tool React.forwardRef(). It allows you to create refin a component Phone, but bind it directly to layout elements in PhoneView. All manipulations as usual refwill then be available in Phone. For example, if we need to connect a phone number validator.

Another feature of this approach is the maximum simplification of the Viewcomponent. In fact, this part is defined as const, without its built-in methods. Only layout and data substitution from the model.

Now our reusable component is divided into Modeland View, we can separately develop business logic and layout code. We can also assemble layout from even smaller component elements.

The state of the entire application running on components


It was shown above that the application can manage components both by passing parameters or typesetting, and by using callbacks.

For the application to work successfully, the upper level needs to receive meaningful data about the state of the nested reused components. However, this may not be the highest level of the entire application.

If we have a client authorization block and reusable components for entering a login and password in it, the whole application does not need to know what state these simple components are in at any given time. Rather, the authorization block itself can calculate a new state based on the states of simple reused components and pass it up: the authorization block is filled correctly or not.

With a large nesting of components, it is necessary to monitor the organization of work with data in order to always know where the "source of truth" is. I have already written

about some of the difficulties associated with the asynchronous state transition in React . Reusable components should always pass up through the callbacks the data that is required to manage the possible blocks of components. However, you do not need to transfer extra data so as not to cause unnecessary redraws of large parts of the DOM tree and not complicate the code for processing changes in components. Another approach to organizing data is to use a component invocation context. This is a native React method. , available from version 16.3, not to be confused with an earlier one ! ..



createContextReact getChildContext

Then you don’t have to pass props through the “thickness” of components down the tree of component nesting. Or use specialized libraries for data management and change delivery, such as Redux and Mobx (see the article on the Mobx + React bundle ).

If we build a library of reusable components on Mobx, each of the types of such components will have its own Store. That is, the "source of truth" about the state of each instance of the component, with end-to-end access from anywhere in the entire application. In the case of Redux and its only data warehouse, all states of all components will be available in one place.

Some ready-made libraries of React components


There are popular libraries of ready-made components, which, as a rule, were originally internal projects of companies:

  1. Material-UI — , Material Design Google.
  2. React-Bootstrap — , . : API , , .
  3. VKUI — «». VK mini apps, (. VK mini apps). VKUI «». «» . vkconnect — iOS Android.
  4. ARUI Feather — React- -. , . open source, .

All of these libraries are aimed at building layouts of elements and their styling. Interaction with the environment is configured using callbacks. Therefore, if you want to create full-fledged reusable components described in the third and fourth paragraphs of the article, you will have to do it yourself. Perhaps, taking as components of the View of such components one of the popular libraries presented above.

This article was written with the support of Mail.ru Cloud Solutions .


All Articles