- PVSM.RU - https://www.pvsm.ru -

Не пиши одно и то же: как переиспользуемые компоненты React помогут фронтенд-разработчику быстрее создавать приложения

Не пиши одно и то же: как переиспользуемые компоненты React помогут фронтенд-разработчику быстрее создавать приложения - 1

Вносить однотипные изменения в три-четыре разных места в JS-коде — искусство, требующее концентрации внимания. Если элементов больше, поддержка кода превращается в муку. Поэтому для долгосрочных или крупных проектов следует писать код так, чтобы его можно было вынести в отдельные компоненты.

Я занимаюсь фронтенд-разработкой уже 10 лет и расскажу о применении компонентов для создания элементов фронтенда — это значительно упрощает жизнь фронтенд-разработчика.

Написано при поддержке Mail.ru Cloud Solutions [1].

Что такое компоненты фронтенд и зачем они нужны

HTML-теги — условно «нулевой» уровень компонентов. У каждого из них свои функции и назначение.

CSS-классы — следующий уровень абстракции, на который обычно выходят при создании даже небольшого сайта. В правилах применения стилей к CSS-классу мы описываем поведение всех элементов, которые входят в условное подмножество элементов.

Правила, применяемые к CSS-классам, а также любым другим элементам, например HTML-тегам, позволяют централизованно задавать и изменять правила отображения любого количества однотипных элементов. Есть разные инструменты для работы со стилями элементов — собственно CSS, Sass, LESS, PostCSS, и методологии применения стилей — БЭМ, SMACSS, Atomic CSS, CSS Modules, Styled components.

Собственно компоненты — это:

  • однотипные элементы, которые имеют как одинаковые стили, так и одинаковую верстку (HTML) и поведение (JS);
  • похожие по стилям и поведению элементы, которые незначительно отличаются друг от друга.

Сейчас развивается технология Web Components, которая позволяет делать кастомные HTML-теги, включить в верстку шаблонные куски кода. Однако компоненты стали широко применяться благодаря современным фреймворкам фронтенд-разработки, таким как Angular, Vue, React. Возможности JavaScript позволяют легко подключить компонент:

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

Все крупные проекты приходят к своей библиотеке готовых компонентов либо к использованию одной из готовых. Вопрос, когда нужно переходить от копирования кода к созданию компонентов, решается индивидуально, здесь нет однозначных рецептов.

Стоит помнить не только о написании кода, но и его поддержке. Простым copy/paste однотипной верстки и изолированием стилей в CSS-классах можно какое-то время создавать отображение без особых рисков. Но если к каждому из элементов добавляется логика поведения, написанная на JS, выгоды от переиспользования кода ощущаются буквально со 2-3 элемента, особенно когда дело касается поддержки и модификации ранее написанного кода.

Переиспользуемые компоненты React

Предположим, наше приложение стало достаточно большим, и мы решили написать свою библиотеку компонентов. Предлагаю использовать для этого популярный инструмент фронтенд-разработки React. Одно из его преимуществ — возможность просто и эффективно использовать вложенные компоненты. В коде ниже старший компонент App использует три вложенных компонента: 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}
               />
           </>
       )
   }
}

Обратите внимание: теперь не требуется использовать в верстке старший оборачивающий тег — обычно это был div. Современный React предлагает инструмент Fragment, сокращенная запись которого <></>. Внутри этих тегов можно использовать плоскую иерархию тегов, как в примере выше.

Мы использовали три библиотечных компонента, один из которых два раза в одном блоке. Данные из родительского приложения передаются в props компонента и будут доступны внутри него через свойство this.props. Такой подход является типовым для React и позволяет быстро собирать отображение (View) из типовых элементов. Особенно если в вашем приложении много похожих страниц, отличающихся только содержимым статей (Model) и функциональностью.

Однако нам может потребоваться модифицировать библиотечный компонент. Например, одинаковые по функциональности компоненты могут отличаться не только текстовым содержимым, но и оформлением: цвет, отступы, границы. Также можно предусмотреть разные варианты функциональности одного и того же компонента.

Ниже рассмотрен такой случай: в зависимости от наличия коллбэка наш компонент может быть «отзывчивым» или оставаться просто View для отрисовки элемента на странице:

// 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>
           }
        )
    }
} 

В React существует еще одна техника расширения возможности компонентов. В параметрах вызова можно передавать не только данные или коллбэки, но и целиком верстку:

// 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>
        )
    }
}

Внутренняя верстка компонента будет воспроизведена целиком так, как она была передана в props.

Чаще удобнее передавать дополнительную верстку в библиотечный компонент с помощью паттерна «вставка» и использования this.props.children. Такой подход лучше для модификации общих компонентов, отвечающих за типовые блоки приложения или сайта, где предполагается различное внутреннее наполнение: шапки, сайдбара, блока с рекламой и других.

// 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>
        )
    }
} 

Полноценные компоненты на React

Выше были рассмотрены компоненты, которые отвечают только за View. Однако нам, скорее всего, потребуется выносить в библиотеки не только отображение, но и стандартную логику обработки данных.

Давайте рассмотрим компонент Phone, который предназначен для ввода номера телефона. Он может маскировать вводимый номер c помощью подключаемой библиотеки-валидатора и сообщать старшему компоненту, что телефон введен правильно или неправильно:

// 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>
        )
    }

} 

Этот компонент уже имеет внутреннее состояние state, частью которого он может делиться с вызвавшим его внешним кодом. Другая часть остается внутри компонента, в примере выше это onceValidated. Таким образом, часть логики работы компонента заключена полностью в нем самом.

Можно сказать, что типовое поведение не зависит от других частей приложения. Например, в зависимости от того, был номер провалидирован или нет, мы можем показывать разные тексты подсказок. Мы вынесли в отдельный переиспользуемый компонент не только отображение, но и логику обработки данных.

MV-компоненты

Если наш типовой компонент поддерживает расширенную функциональность и имеет достаточно развитую логику поведения, то стоит разделить его на два:

  • «умный» для работы с данными (Model);
  • «глупый» для отображения (View).

Подключение будет происходить по-прежнему путем вызова одного компонента. Теперь это будет Model. Вторая часть — View — будет вызываться в render() с props, часть из которых пришла из приложения, а другая часть уже является state самого компонента:

// 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;

Стоит обратить внимание на инструмент React.forwardRef(). Он позволяет создавать ref в компоненте Phone, но привязывать его непосредственно к элементам верстки в PhoneView. Все манипуляции как с обычным ref будут в таком случае доступны в Phone. Например, если нам нужно подключить валидатор номера телефона.

Другой особенностью такого подхода является максимальное упрощение View компонента. Фактически эта часть определена как const, без своих встроенных методов. Только верстка и подстановка данных из модели.

Теперь наш переиспользуемый компонент разделен на Model и View, мы можем отдельно разрабатывать код бизнес-логики и верстки. Мы также можем собирать верстку из ещё более мелких компонентов-элементов.

Состояние всего приложения, выполненного на компонентах

Выше было показано, что приложение может управлять компонентами как с помощью передачи параметров или верстки, так и с помощью коллбэков.

Для успешной работы приложения требуется, чтобы верхний уровень получал значимые данные о состоянии вложенных переиспользуемых компонентов. При этом это может быть не самый верхний уровень всего приложения.

Если у нас есть блок авторизации клиента, и в нем переиспользуемые компоненты для ввода логина и пароля, всему приложению не надо знать, в каком состоянии находятся эти простые компоненты в каждый конкретный момент времени. Скорее, сам блок авторизации может вычислять новое состояние, основанное на состояниях простых переиспользуемых компонентов, и передавать его наверх: блок авторизации заполнен корректно или нет.

При большой вложенности компонентов требуется следить за организацией работы с данными, чтобы всегда знать, где находится «источник истины».

О некоторых трудностях, связанных с асинхронностью изменения состояния в React, я уже писал [2].

Переиспользуемые компоненты всегда должны передавать наверх через коллбэки данные, которые требуются для управления возможными блоками компонентов. Однако не нужно передавать лишние данные, чтобы не вызывать ненужных перерисовок больших частей DOM-дерева и не усложнять код обработки изменений в компонентах.

Другой подход к организации данных заключается в использовании контекста вызова компонентов. Это нативный метод React.createContext, доступный с версии 16.3, не путать с более ранним React getChildContext!..

Тогда не придется передавать props через «толщу» компонентов вниз по дереву вложенности компонентов. Либо использовать специализированные библиотеки управления данными и доставки изменений, такие как Redux и Mobx (см. статью о связке Mobx + React [3]).

Если мы построим библиотеку переиспользуемых компонентов на Mobx, у каждого из типов таких компонентов будет свой Store. То есть «источник истины» о состоянии каждого экземпляра компонента, со сквозным доступом из любого места во всем приложении. В случае с Redux и его единственным хранилищем данных все состояния всех компонентов будут доступны в одном месте.

Некоторые готовые библиотеки React-компонентов

Существуют популярные библиотеки готовых компонентов, которые, как правило, изначально были внутренними проектами компаний:

  1. Material-UI [4] — набор различных компонентов, реализующих подход к дизайну Material Design от Google.
  2. React-Bootstrap [5] — еще одна библиотека, реализующая популярный подход к созданию и стилизации интерфейсов. Обе библиотеки имеют огромное количество последователей по всему миру и во многом схожи по устройству: обширное API использования каждого из компонентов, хорошая документация с примерами, возможность переопределять стили элементов.
  3. VKUI [6] — библиотека компонентов сети «ВКонтакте». Она используется в сторонних приложениях VK mini apps, запускаемых внутри соцсети (см. подробный материал по VK mini apps [7]). Стили компонентов VKUI практически неотличимы от стилей нативного приложения «ВКонтакте». Это возможность использовать «бесшовный» переход от страниц ВК к страницам вашего приложения и обратно. Вместе с библиотекой vkconnect эта библиотека — сильный инструмент для построения приложений с учетом особенностей дизайна под iOS и Android.
  4. ARUI Feather [8] — проект React-компонентов Альфа-банка. Это библиотека типовых расширяемых компонентов, которые банк использует для разработки интерфейсов. Библиотека позиционируется как open source, каждый может стать контрибьютором [9].

Все указанные библиотеки нацелены на построение верстки элементов и их стилизацию. Взаимодействие с окружением настраивают с помощью коллбэков. Поэтому если вы хотите создать полноценные переиспользуемые компоненты, описанные в третьем и четвертом пунктах статьи, придется сделать это самим. Возможно, взяв в качестве элементов View таких компонентов одну из популярных библиотек, представленных выше.

Статья написана при поддержке Mail.ru Cloud Solutions [1].

Автор: Максим

Источник [13]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/349233

Ссылки в тексте:

[1] Mail.ru Cloud Solutions: https://mcs.mail.ru/

[2] я уже писал: https://mcs.mail.ru/blog/razrabotka-na-react-kak-izbezhat-oshibok

[3] статью о связке Mobx + React: https://mcs.mail.ru/blog/plyusy-svyazki-mobx-react-dlya-upravleniya-sostoyaniem-prilozheniya

[4] Material-UI: https://material-ui.com/

[5] React-Bootstrap: https://react-bootstrap.github.io/

[6] VKUI: https://vkcom.github.io/vkui-styleguide/

[7] подробный материал по VK mini apps: https://mcs.mail.ru/blog/razrabotka-prilozhenij-v-vk-mini-apps

[8] ARUI Feather: https://github.com/alfa-laboratory/arui-feather

[9] может стать контрибьютором: https://github.com/alfa-laboratory/arui-feather/blob/master/.github/CONTRIBUTING.md

[10] Мой второй год в качестве независимого разработчика: https://habr.com/ru/post/487064/

[11] Полезные инструменты командной строки: https://mcs.mail.ru/blog/illyustrirovannoe-rukovodstvo-po-poleznym-instrumentam-komandnoj-stroki

[12] Как технический долг убивает ваши проекты: https://habr.com/ru/company/mailru/blog/486098/

[13] Источник: https://habr.com/ru/post/491174/?utm_source=habrahabr&utm_medium=rss&utm_campaign=491174