ZarahioN Presents

Answering why

Пора умнеть [Pt. 5 — React Statefull Components, variation Uno]

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

Небольшие правки были совершены в трактовке и тавтологии понятий 4 Апреля 2018

Напомню, что в используемой мной системе понятий, глупые компоненты — это компоненты, которые не имеют своей не визуальной логики «кхм, правка тройного отрицания — глупые компоненты, это компоненты, которые если и имеют логику, то только свою и визуальную». Вроде бы просто «нет», но что означает «своя (не) визуальная логика»?

Во-первых, своя означает, что компонент должен обладать влияющей не только на него (его состояние) логикой, которую он провоцирует, чтобы быть «умным». То есть, глупый компонент остается глупым, даже если он играется, но только в своей песочнице.

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

В-третьих, обе части неравноправия используются вместе — если у компонента есть визуальная логика, задевающая другие части приложения (спорно, если другие части это прямые «дети»), то он уже не может считаться глупым. И так же если компонент что-то считает, делает запросы или обрабатывает пользовательский ввод, при этом пусть и отображая результаты только в себе, он тоже уже не может счиаться глупым (но он остается глупым, если ввод обрабатывает родитель, передавая компоненту только данные и ссылки (reference) на обработчики).

Таким образом терминология получается несколько сложнее, чем просто разделение есть состояние/нету, или есть функции/нету, однако такой набор правил позволяет чуть строже и надёжнее разграничить области влияния глупых и умных компонентов.
И как всегда — это лишь то разделение, к которому я пришел со временем, поглядывая на Flux, Redux и прочих влиятельных игроков на поле деятельности. Каждый волен использовать свои понятия, до тех пор, пока их ясно осознают все члены команды.

Что же, думаю, пора кончать болботать и начинать создавать. Начнем с простенького, форма:

@index.js
import ...
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));

@App.js
import React, { Component } from 'react';
import './App.css';

const LabeledInput = ({children, name, value, onChange, type="text"}) =>
<div className="input-group">
    <label htmlFor={name}>{children} </label>
    <input type={type} id={name} name={name} value={value} onChange={onChange}/>
</div>

class App extends Component {
    // constructor(p) {
    //     super(p);
    //     this.state = {
    //         demo: '',
    //         name: '',
    //         password: ''
    //     }
    //  this.handleInput = this.handleInput.bind(this) // привязка при использовании методов класса
    // }
    // handleInput(e) {
    //  this.setState({[e.target.name]: e.target.value})
    // }
    state = {
        demo: '',
        name: '',
        password: ''
    }
    render() {
        const handleInput = (e) => this.setState({
            [e.target.name]: e.target.value
        });
        const {
            demo, name, password
        } = this.state;
        return (<div className="form-container App">
            <form action="" onSubmit={(e) => e.preventDefault()}>
                <LabeledInput value={demo} name="demo" onChange={handleInput}>
                    First input label
                </LabeledInput>
                <LabeledInput value={name} name="name" onChange={handleInput}>
                    Namu?
                </LabeledInput>
                <LabeledInput value={password} name="password"onChange={handleInput} type="password">
                    Passwort
                </LabeledInput>
            </form>
            <div class="output">
                {
                    Object.entries(this.state).map(arr =>
                        arr[1] !== '' && <div>{arr[0]}: {arr[1]}</div>
                    )
                }
            </div>
        </div>);
    }
}

export default App;

Как можно сразу заметить — я вынес код приложения из index.js. Это стандартная практика — в index.js помещают конфигурацию и вызов ReactDOM.render корневого (root) элемента приложения, который часто является провайдером для роутера, состояния или их обоих.

В самом приложении (App.js) находится сразу два компонента — глупый для отображения отдельного поля в форме и умный для самой формы — LabeledInput и App соответственно. Помимо уже знакомых и вручную переданных через JSX атрибуты свойств (props), в LabeledInput присутствует еще одна деструктуризированная переменная children.

Children это специальное свойство (элемент объекта this.props), в которое реакт передает всех потомков JSX элемента:

<LabeledInput value={demo} name="demo" onChange={handleInput}>
    First input label
</LabeledInput>

В данном случае это просто текст «First input label» между открывающим и закрывающим тегом.
Сюда можно передавать любой JSX элемент — простой текст, будь то прописанный нами литерал или переменная, стандартные HTML элементы и другие JSX компоненты. При этом компонент получивший таких потомков сам решает что с ними сделать — в нашем случае мы просто вывели их в label, однако при желании и должной сноровке можно, к примеру, обработать, отфильтровать, или использовать их для каких-либо проверок внутри компонента.

Следующей примечательной остановкой является переменная\поле класса — state. Согласен, удивительно, но это именно то, что отличает statefull компоненты от stateless.
По сути state это объект (как правило; технически это просто переменная JS) представляющий «состояние» компонента — все данные нужные для его внутренней работы. Это чисто наш объект — реакт никак не вмешивается в его работу и узнает о его изменениях только в случае предназначенного обновления через специальный метод компонента setState. Альтернативно можно использовать forceUpdate() и прочие непотребства, но естественно это не рекомендуется в обычной практике.

Должен заметить что, насколько я помню, переменные класса не вошли в стандарт ES6 и могут быть недоступны в зависимости от настроек бейбла, впрочем, я могу ошибаться. Однако, если я не ошибся, то тебе возможно придется использовать классический синтаксис и инициализировать состояние через конструктор, я включил примерный код в виде комментария. Учитывая что наш класс расширяет компонент реакта, в самом начале конструктора нужно вызывать super передав в него полученные аргументом свойства (props) p. (Признаюсь, меня никогда не интересовало почему нужно так делать, поэтому объяснить это я не смогу)

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

const handleInput = (e) => this.setState({ [e.target.name]: e.target.value });

Это все та же любимая стрелочная функция, она получает исскуственный объект события и обновляет состояние установив по ключу в виде названия инпута (name) его значение (value). Я опять же использую один из ES трюков — переменную в виде ключа объекта.

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

Еще один важный момент связанный со стрелочными функциями: я упоминал, что их почти всегда можно напрямую заменить обычными. В данном случае это не так — мы используем ключевое слово this в обработчике, чтобы ссылаться на отображенный компонент (или экземпляр класса), а классические функции в JS всегда создают свое поле деятельности this(, или используют предоставленное им поле объекта, если это методы, а еще они могут… в классическом JS с this все достаточно запутано и сложно, если вкратце).

В реакте (и в JS вообще) такое поведение часто не нужно и вызывает вопросы, поэтому одна из главных причин использования стрелочных функций в том, что они не создают свою игровую площадку this, а используют предоставленную в непосредственной области видимости — в нашем случае это наш компонент (или метод компонента, если придираться очень сильно), именно то, что нам и нужно.
Однако, использовать классические функции можно, при этом есть как минимум два способа:

  1. Классика и создание связывающей ссылки вроде переменной self, принцип простой — перед определением функции мы создаем «переменную» к которой привязываем нужный нам вариант this и используем уже эту переменную вместо прямого обращения к this, который был «перезаписан» функцией.
  2. Чуть более элегантный и признанный — создания «присвоенной» копии функции через специальный метод bind. Он позволяет указать что использовать функции в качестве this (или точнее просто привязать ссылку this внутри функции на то, что нужно нам). Помимо этого bind позволяет (и то зачем я куда чаще его использую) добавить аргументы в функцию, к примеру:

start="1.">
const reducingFunction = (incrementing , acc, elem) => (incrementing ? acc + elem : acc - elem)
let a = [1,2,3,4,5].reduce( reducingFunction.bind(null, true), 0 );
// a -> 15

Позволяет создать одну функцию для обработки нескольких типов данных или случаев применения. С таким простым вариантом как сумматор это, конечно, бессмысленно, однако позволяет делать интересные (и страшные) вещи с помощью простого reduce или forEach, возможно в будущем я покажу пару моих безобразных «изобретений». При этом я передал null первым аргументом bind, так как this внутри нашего сумматора мы не только не используем, но стрелочные функции даже не имеют понятия, что это.

Однако, возвращаемся после нашего краткого экскурса в страшные детали работы JS к доброму-мирному реакту.

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

const { demo, name, password } = this.state;

Ничего особо нового, просто источник данных сменился со свойств (props) на состояние (state).

И наконец разметка, в которой все достаточно просто — у нас есть форма, внутри нее наши «глупые» компоненты которым передано все самое важное: значения и названия вводов(инпутов input), «ссылка» на обработчик и условная «подпись» поля через потомка. Условная потому что, как я уже говорил, поведение children определяет сам компонент — если вручную не вывести его где-либо в разметке, он вообще не станет отображаться.

Небольшим бонусом в самом конце я добавил вывод состояния через (очередную технически не полностью поддерживаемую) функцию (точнее статический метод, что не суть) Object.entries, обладающую двумя младшими братьями — Object.keys и Object.values, у них поддержка, кажется, чутка лучше. Суть у нее проста — взять объект и вернуть массив «кортежей» (но на самом деле массивов, мы же не развлекаем змеек) в виде [ключ, значение].
На тему поддержки — мы в царстве бейбла и технически можем о ней не волноваться, ибо все что нужно будет скомпилировано в простонародные варианты либо подкрашено заполнителями (polyfill), однако учитывая приятность огромного количества (человеческих, кхем) нововведений ES6 и выше, легко можно привыкнуть и писануть чего-нибудь ломающего браузер непросвещенного пользователя. Поэтому я стараюсь по ходу делать рассказывать о всем том, что это ломание любит вызывать и с чем я сам радостно сталкивался (тратя часы на отладку с пользователем устаревающего сафари, который при этом просто так не поставить себе для испытаний, я всегда недолюбливал яблоки).

Отставив стоны и стенания, запускаем
yarn start
И можем любоваться на нашу скромнейшую форму.
Но тыкать куда интереснее, поэтому не забудь ввести что-нибудь в каждое поле и порадоваться «магическому» обновлению небольшого отладочного вывода.

К слову о отладке, есть небольшой трюк для логгирования (и не только) в JS:

const LabeledInput = ({children, name, value, onChange, type="text"}) =>
console.log(children) || <div className="input-group">
    <label htmlFor={name}>{children} </label>
    <input type={type} id={name} name={name} value={value} onChange={onChange}/>
</div>

Так как console.log не возвращает значений он приравнивается к false и из-за поведения логического присваивания в JS функция возвращает не undefined а правильное значение после || — JSX разметку нашего компонента.

Итак, мое любимое повторение. Чего мы наваяли:
1. Создали полноценный компонент (класс) с помощью class X extends Component
2. Создали функциональный компонент для каждого именованного ввода с помощью стрелочной функции const Y = ({...props}) =>
3. В умном компоненте инициализировали состояние через поле класса state
4. В отображении (рендере) умного компонента создали обработчик полей ввода и деструктуризовали значения из его состояния
5. Вернули в умном компоненте разметку, в которой использовали функциональный компонент, передав его экземплярам данные и обработчик.
(6. Надеюсь, потискали получившуюся форму)

Что же, на этом, я думаю, пора завершать наше нескромно распухлевшее введение в умнеющие компоненты реакта. Я скорее всего предоставлю еще несколько примеров и пояснений классических вариантов использования реакта (в том числе покажу как можно реализовать отправку нашей формы и наукрашатильствовать ее по-максимуму) по ходу развития и рассусоливания всей темы.

Ну а пока — до встречи, надеюсь скорой.

Comments

  • [Pt. 6 – Why states] – Answering why | Мар 26,2018

    […] же, не так давно, в прошлой статье мы познакомились с Store – Состоянием (я о модели Flux) в […]

    Leave a Reply

    Ваш e-mail не будет опубликован. Обязательные поля помечены *