Наши лучшие практики написания компонентов React
08.03.2017

Наши лучшие практики написания компонентов React

Оригинал: https://engineering.musefind.com/our-best-practices-for-writing-react-components-dec3eb5c3fc8#.4t8xj0omj, Scott Domes

Я помню, когда я только начинал писать на React, я видел много разных подходов к построению компонентов, сильно различающихся от учебника до учебника. И хотя с тех пор фреймворк значительно созрел, похоже, до сих пор не существует единственного «правильного» способа написания компонентов.

За последний год в MuseFind наша команда написала много компонентов React. Мы постепенно улучшаем наш подход, пока он не будет полностью нас устраивать.

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

Прежде чем мы начнем, несколько замечаний:

  • Мы используем синтаксис ES6 и ES7.
  • Если вы не уверены в том, чем отличаются презентационные компоненты и компоненты контейнеры, прочтите это.
  • Пожалуйста, дайте нам знать в комментариях, если у вас есть предложения, вопросы или обратная связь.

Компоненты на основе классов

Компоненты на основе классов имеют состояние и/или содержат методы. Мы стараемся использовать их как можно реже, но и для них находится свое применение.

Давайте пошагово построим наш компонент, строка за строкой.

Импорт CSS


import React, {Component} from 'react'
import {observer} from 'mobx-react'

import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

Мне нравится CSS в JavaScript, в теории. Но это все еще новая идея, и зрелое решение не появилось. До тех пор мы импортируем файл CSS для каждого компонента.

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

Инициализация состояния


import React, {Component} from 'react'
import {observer} from 'mobx-react'

import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'


export default class ProfileContainer extends Component {
    state = { expanded: false }

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

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

propTypes and defaultProps


import React, {Component} from 'react'
import {observer} from 'mobx-react'

import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

export default class ProfileContainer extends Component {
    state = { expanded: false }

    static propTypes = {
        model: React.PropTypes.object.isRequired,
        title: React.PropTypes.string
    }

    static defaultProps = {
        model: {
            id: 0
        },
        title: 'Your Name'
    }

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

Все компоненты должны иметь propTypes.

Методы


import React, {Component} from 'react'
import {observer} from 'mobx-react'

import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

export default class ProfileContainer extends Component {
    state = { expanded: false }

    static propTypes = {
        model: React.PropTypes.object.isRequired,
        title: React.PropTypes.string
    }

    static defaultProps = {
        model: {
            id: 0
        },
        title: 'Your Name'
    }

    handleSubmit = (e) => {
        e.preventDefault()
        this.props.model.save()
    }

    handleNameChange = (e) => {
        this.props.model.changeName(e.target.value)
    }

    handleExpand = (e) => {
        e.preventDefault()
        this.setState({ expanded: !this.state.expanded })
    }

В компонентах на основе классов, когда метод передается в дочерний компонент, необходимо убедиться, что этот метод будет использовать правильный this при вызове. Обычно это достигается передачей this.handleSubmit.bind(this) в дочерний компонент.

Мы считаем, что проще и чище поддерживать правильный контекст автоматически с помощью стрелочных функций ES6.

Деструктуризация свойств


import React, {Component} from 'react'
import {observer} from 'mobx-react'

import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

export default class ProfileContainer extends Component {
    state = { expanded: false }

    static propTypes = {
        model: React.PropTypes.object.isRequired,
        title: React.PropTypes.string
    }

    static defaultProps = {
        model: {
            id: 0
        },
        title: 'Your Name'
    }

    handleSubmit = (e) => {
        e.preventDefault()
        this.props.model.save()
    }

    handleNameChange = (e) => {
        this.props.model.changeName(e.target.value)
    }

    handleExpand = (e) => {
        e.preventDefault()
        this.setState(prevState => ({ expanded: !prevState.expanded }))
     }

    render() {
        const {
            model,
            title
        } = this.props
        return (
            <ExpandableForm 
                onSubmit={this.handleSubmit}
                expanded={this.state.expanded}
                onExpand={this.handleExpand}>
                <div>
                    <h1>{title}</h1>
                    <input
                        type="text"
                        value={model.name}
                        onChange={this.handleNameChange}
                        placeholder="Your Name"/>
                 </div>
            </ExpandableForm>
        )
    }
}

Для компонентов с несколькими свойствами каждое свойство указываем с новой строки.

Декораторы


@observer
export default class ProfileContainer extends Component {

Если вы работаете с чем-то вроде mobx, вы можете использовать декораторы вместо передачи компонента в функцию.

Декораторы — это гибкий и понятный способ изменения функциональности компонента. Мы их широко используем с mobx и нашей библиотекой mobx-models.

Если вы не хотите использовать декораторы, можете сделать следующее:


class ProfileContainer extends Component {
 // Component code
}
export default observer(ProfileContainer)

Замыкания

Избегайте передачи нового замыкания в дочерний компонент как показано ниже:


<input
    type="text"
    value={model.name}
    // onChange={(e) => { model.name = e.target.value }}
    // ^ Not this. Use the below:
    onChange={this.handleChange}
    placeholder="Your Name"/>

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

Если бы input был компонентом React, это автоматически вызывало бы его перерисовку независимо от того, изменились другие его свойства или нет.

Сравнение DOM — самая дорогостоящая часть React. Не делайте его сложнее, чем необходимо! Кроме того, при передаче метода класса, код легче читать, отлаживать и изменять.

Вот наш полный компонент:


import React, {Component} from 'react'
import {observer} from 'mobx-react'
// Отделяем локальный импорт от зависимостей
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

// Используем декораторы, если необходимо
@observer
export default class ProfileContainer extends Component {
    state = { expanded: false }
    // Инициализируем состояние здесь (ES7) или в конструкторе (ES6)
 
    // Определяем propTypes как статическое свойство как можно раньше
    static propTypes = {
        model: React.PropTypes.object.isRequired,
        title: React.PropTypes.string
    }

    // Определяем свойства по умолчанию после propTypes
    static defaultProps = {
        model: {
            id: 0
        },
        title: 'Your Name'
    }

    // Используем стрелочные функции, чтобы сохранить контекст
    handleSubmit = (e) => {
        e.preventDefault()
        this.props.model.save()
    }
  
    handleNameChange = (e) => {
        this.props.model.name = e.target.value
    }
  
    handleExpand = (e) => {
        e.preventDefault()
        this.setState(prevState => ({ expanded: !prevState.expanded }))
    }
  
    render() {
        // Деструктуризация свойств для повышения читаемости
        const {
            model,
            title
        } = this.props
        return ( 
            <ExpandableForm 
                onSubmit={this.handleSubmit} 
                expanded={this.state.expanded} 
                onExpand={this.handleExpand}>
                // Отделяем новой строкой, если свойств больше двух
                <div>
                    <h1>{title}</h1>
                    <input
                        type="text"
                        value={model.name}
                        // onChange={(e) => { model.name = e.target.value }}
                        // Избегайте создания замыканий в методе render - используйте методы
                        onChange={this.handleNameChange}
                        placeholder="Your Name"/>
                </div>
            </ExpandableForm>
        )
    }
}

Функциональные компоненты

Эти компоненты не имеют состояния и методов, поэтому они простые и чистые. Используйте их как можно чаще.

propTypes


import React from 'react'
import {observer} from 'mobx-react'

import './styles/Form.css'

ExpandableForm.propTypes = {
    onSubmit: React.PropTypes.func.isRequired,
    expanded: React.PropTypes.bool
}

// Component declaration

Мы определяем propTypes до объявления компонента, чтобы они были сразу видны. Мы можем делать это благодаря подъему функций в JavaScript.

Деструктуризация свойств и defaultProps


import React from 'react'
import {observer} from 'mobx-react'

import './styles/Form.css'

ExpandableForm.propTypes = {
    onSubmit: React.PropTypes.func.isRequired,
    expanded: React.PropTypes.bool
}

function ExpandableForm(props) {
    return (
        <form style={props.expanded ? {height: 'auto'} : {height: 0}}>
            {props.children}
            <button onClick={props.onExpand}>Expand</button>
        </form>
    )
}

Наш компонент — это функция, которая принимает props в качестве параметра. Мы можем доработать его следующим образом:


import React from 'react'
import {observer} from 'mobx-react'

import './styles/Form.css'

ExpandableForm.propTypes = {
    onExpand: React.PropTypes.func.isRequired,
    expanded: React.PropTypes.bool
}

function ExpandableForm({ onExpand, expanded = false, children }) {
    return (
        <form style={ expanded ? { height: 'auto' } : { height: 0 } }>
            {children}
            <button onClick={onExpand}>Expand</button>
        </form>
    )
}

Обратите внимание, как мы использовали параметры по умолчанию, чтобы задать defaultProps удобным способом. Если свойство expanded неопределено, устанавливаем его значение false (немного притянутый пример, поскольку это логическое выражение, но он наглядно показывает, как можно избежать ошибок ‘Cannot read of undefined’ объекта).

Избегайте следующего синтаксиса ES6:


const ExpandableForm = ({ onExpand, expanded, children }) => {

Выглядит очень современно, но функция фактически анонимная.

Это отсутствие имени не вызовет проблем, если ваш Babel настроен правильно — а если нет, любая ошибка будет отображаться как произошедшая в <>, что очень неудобно для отладки.

Также анонимные функции вызывают проблемы с Jest, библиотекой для тестирования React. Из-за потенциально трудных для понимания ошибок (и отсутствия реальной пользы) мы рекомендуем использовать function вместо const.

Обертка

Поскольку нельзя использовать декораторы с функциональными компонентами, вы можете просто передать функцию как параметр:


import React from 'react'
import {observer} from 'mobx-react'

import './styles/Form.css'

ExpandableForm.propTypes = {
    onExpand: React.PropTypes.func.isRequired,
    expanded: React.PropTypes.bool
}

function ExpandableForm({ onExpand, expanded = false, children }) {
    return (
        <form style={ expanded ? { height: 'auto' } : { height: 0 } }>
             {children}
             <button onClick={onExpand}>Expand</button>
        </form>
    )
}

export default observer(ExpandableForm)

Полный код компонента:


import React from 'react'
import {observer} from 'mobx-react'
// Отделяем локальный импорт от зависимостей
import './styles/Form.css'

// Определяем propTypes здесь перед компонентом (благодаря подъему функций в JS)
// Они должны быть сразу заметны
ExpandableForm.propTypes = {
    onSubmit: React.PropTypes.func.isRequired,
    expanded: React.PropTypes.bool
}

// Деструктуризация свойств и использование параметров по умолчанию в качестве defaultProps
function ExpandableForm({ onExpand, expanded = false, children }) {
    return (
        <form style={ expanded ? { height: 'auto' } : { height: 0 } }>
            {children}
            <button onClick={onExpand}>Expand</button>
        </form>
    )
}

// Обертка вместо декоратора
export default observer(ExpandableForm)

Условные выражения в JSX

Скорее всего вы часто используете условные выражения в методе render. Вот чего следует избегать:

Нет, вложенные тернарные операторы — это плохая идея.

Есть несколько библиотек, которые решают эту проблему (JSX-Control Statements), но вместо того, чтобы вводить новую зависимость, мы остановились на таком подходе для сложных условий:

Используйте фигурные скобки, обертывающие IIFE, а все выражения if поместите внутрь, возвращая все, что должно отображаться. Обратите внимание, что IIFE подобным образом может привести к потере производительности. Но в большинстве случаев потеря производительности не будет достаточно значительной, чтобы перевесить потерю коэффициента удобочитаемости.

Обновление: Многие комментаторы рекомендовали выделить эту логику в дочерний компонент, который в зависимости от условия будет возвращать разные кнопки на основе свойств. Они правы, максимальное разделение ответственности компонентов — это хорошая идея. Но помните о подходе с IIFE в качестве запасного варианта.

Также если вы хотите отобразить компонент в зависимости от условия, вместо этого ...


{
    isTrue
        ? <p>True!</p>
        : <none/>
}

… используйте:


{
 isTrue &&
   <p>True!</p>
}