ZarahioN Presents

Answering why

Первое настоящее приложение с React.js [Pt. 7 — Todo App!]

Мы подобрались к важной составляющей любого приложения — Состоянию — очень быстро, и я уже думал начать разбирать редакс, однако вспомнил, что хотел показать как можно написать приложение в «трех стилях»: классическом реакте, без библиотек Состояния, с редаксом, как классикой решения проблемы, и современным подходом в виде MobX и его наблюдаемыми коллекциями (observable).

Плюс перед выходом к редаксу или моби (да-да, бедный MobX), не помешает нарядить и прихорошить свой скромный опыт работы с реактом.

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

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

Для начала, я решил предоставить краткое описание важных элементов получившегося монстра приложения, для тех кому ленно зайти в репо и глянуть код:
— У нас есть TodoApp(T)/index.js — базовый импорт всего приложения, который (по уму) должен создавать списки тудушек и подключать стили, но изначально бодяжил вообще все, о чем подробнее в графе ошибок,
T/TodoList.js(x) — изначально глупое Представление списка тудушек, которое благоразумно развилось до умного компонента, держущего список тудушек и следящего за ним, помимо прочего,
T/Todo.jsx — классически глупое Представление отдельной тудушки, которое раскидывает обработчики и данные,
T/AddTodoField.jsx — мое (спорно) глупое Представление поле добавление новой тудушки, о нем тоже подробнее ниже,
— и «мелочи» вроде стилей, моих неказистых CSS иконок и простенького заголовка.

Ожидаемо основной вес, чуть ли не чрезмерный, имеет TodoList — поумневший компонент, который отвечает за список тудушек и все CRUD действия с ними.
CRUD — это часто используемая с БД аббревиатура означающая стандартный набор действий с данными: Создание, Чтение, Изменение и Удаление (Create, Read, Update, Delete).

Интересным образом я заметил у себя полный редакс привычек — даже не используя его, я пытаюсь следовать подобной методике — набор действий и редукторов, которые предсказуемо изменяют состояние в TodoList:

const handlers = {
    onDoneClick: (index, done) => {
        const todoSet = this.state.todos;
        const newTodoSet = todoSet.set(
            index, todoSet.get(index).set('done', done)
        );
        return this.setState({todos: newTodoSet});
    },
    onTodoAdd: (text = '') => {
        if (text === '') return false;
        const todos = this.state.todos;
        this.setState({todos: todos.push(TodoRecord({text}))});
        return true;
    },
    onTodoDelete: (deleting) => {
        if (!isNaN(deleting) && this.state.deletingTodo === deleting){
            handlers.onTodoDeleted(true);
            this.setState({deletingTodo: null});
        }
        this.setState({deletingTodo: isNaN(deleting) ? null : deleting})
    },
    onTodoDeleteCancel: (index) => {
        handlers.onTodoDelete(null);
    },
    ...
};

Ко всему прочему я решил закинуть часто идущий с редаксом «в комплекте» Immutable.js — библиотеку, предоставляющую надежные неизменяеные коллекции и типы данных. Они нужны для поверхностной проверки равенста (shallow equality check) — операций сверения двух коллекций, которая надежно в данном случае определяет является ли это одной и той же коллекцией. Вторым важным преимуществом являтся невозможность такого казуса:

let a = { key: 'var' };
let b = a;
b.key = 37;
// a -> { key: 37 }

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

Почему Immutable, если у нас особо нечего сравнивать и ссылками мы вроде баловаться не собираемся? Все достаточно просто — она позволяет надежно и легко изменить обновлять запись по индексу в массиве-списке (List). Без него редактирование массива превращается в мутации либо в танцы со сплайсом/слайсом (splice, slice) и индексами, что или надоедает, или требует ваяния самодельных костылей.

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

Следующим важным компонентом идет непосредственно сама глупая тудушка — Todo.jsx, можно заметить что я использую два расширения файлов — .js и .jsx, об этом чуть позже.

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

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

{
    editing ? 
    <form className="input-field--wrap" onSubmit={(e) => e.preventDefault() || onTodoEdited(index, editingText)}>
        <input
            value={editingText}
            onChange={ e => oneditingTextChange(index, e.target.value) }
            className="input-field todo-edit"
            type="text"
        />
    </form> :
    [<span key="checkbox-wrap" className="todo-done">
        <input type="checkbox" 
            onChange={
                (e => onDoneClick(index, e.target.checked))
            }/>
        <CheckMarkToggle done={done}/>
    </span>,
    <span
        key="text-wrap"
        className="todo-text"
        onDoubleClick={() => onTodoEdit(index, text)}
    >{text}</span>]
}

Это до глупого элегантный и простой способ выбрать между двумя (и более, если ты любитель лесенок и головоломок) вариантами отображения, для, как у нас, варианта когда что-то редактируется или нет. Суть заключается в том, что {} в JSX позволяют писать любой JS код, возвращающий значения, в том числе и другую JSX разметку.
Таким образом:

const AB = ({isA}) => isA ? <A/> : </B>;

Отобразит компонент A если isA правдиво и компонент B иначе.
Альтернативным и более громоздким (и на удивление менее читаемым и удобным) вариантом будет задать переменную содержащую нужный компонент до рендера с помощью обычного if, но как и многими извращениями, я таким не занимаюсь.

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

Наконец AddTodoField.jsx, который, несмотря на использование полноценного компонента и своего Состоянимя, я продолжаю считать глупым по двум причинам:
1. и основная — Состояние компоненту создавалось для хранения focused значения — что есть яркий пример визуальной логики, когда состояние инпута нужно подсвечивать выше в дереве разметки,
2. и чуть противоречивая — наличие newTodoText в состоянии — если кратко, то из-за его несущественности для родительского компонента и наличия полноценной поддержки из-за необходимости держать значение фокуса я решил в данном конкретном случае не выносить Состояние компонента вверх. Однако в подобных случаях это как правило рекоммендуемый ход действий, так как полноценный компонент добавляет лишний физический вес приложению и потребляемой памяти, но такой ввод у нас один на список и экономия памяти, в моем личном видении ситуации не стоит загрузки родительского компонента лишним Состоянием.

А теперь об ошибках и Post Scriptum-е

1. index.js-казус

По глупости и под приятную музыку, я зачем-то решил наделить излишним умом самый верх приложения, непосредственно index.js, даже зная о том, что планирую в будущем создать несколько списков и перемещать данные между ними. Предполагаю сыграла уже названная привычка редакса — я думал о том, чтобы начинать дерево состояния в корне в виде нескольких ключей-веток под каждый список, и, как говорится «shot myself in da foot» — сел меж двух библиотек, так и не решив идти с «одно Состояние на компонент» в классическом реакте, или с «одно Состояние на приложение с контекстом» в редаксе.

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

2. «Слишком умный» AddTodoField.jsx

Это не столько ошибка, сколько очень близкое приближение к границам понятий — насколько жестко трактовать запись в Состояние значение ввода, если обработкой этого значения занимается родитель?

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

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

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

Почему возникла эта проблема? Все то же влияние редакса и его жесткое разделение компонентов на Контейнеры и Компоненты — первые имеют Состояние и куда важнее возможность создавать Действия, вторые просто отображают данные. И если следовать терминологии редакса, то под любое изменение состояния нужно писать связку Действие-Диспетчер-Новое Состояние, за счет чего и получается порой излишняя его многословность — две строчки, одна на переменную состояния, другая на обновления состояния в классике против пяти-семи строчек в редаксе разбросанные по любимым трем файлам.

Сила редакса не в обновлении значений полей формы, а в упрощении и строгом порядке сложных обновлений. Именно для такого разграничения при использовании подхода редакса я и решил устроиться посередине со своим разделением на умные и глупые компоненты — сложные обновления уровня всего приложения или большого компонента решает редакс, с простыми же визуальными частями Состояния легко справляется и система самого реакта.

О разделении на .js и ,jsx

На данный момент разработчики c-r-a и сообщество реакта не очень рекомендует использовать .jsx расширение по ряду причин, которые мне лень искать. Но два расширения фасилитируют мое разделение глупых и умных компонентов — .js это полноценные умные компоненты, .jsx это глупые, как правило функциональные, компоненты. Такое физическое разделение чисто семантическое и при необходимости можно просто пакетно переименовать все файлы — система импорта вебпака проверяет все сконфигурированные расширения тем самым позволяя опускать его при импорте — import Todo from './Todo'.

By the way, I may create simple «magic app loader» который позволит загружать ваши реализации Todo-приложения из репо, если предложений придет достаточно много — тогда любой сможет посмотреть на все вариации на единой странице, а я покажу как сделать выделенный загрузчик реакт компонентов и дополнительно буду вынужден показать чанкование и использование внешних библиотек.

На сим я бы предложил откланиться и бежать допиливать свое Todo-100500-ое-приложение.

Leave a Reply

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