Добрый день, меня зовут Павел Поляков, я Principal Engineer в каршеринг компании SHARE NOW, в Гамбурге в ???????? Германии. А еще я автор телеграм канала Хороший разработчик знает, где рассказываю обо всем, что обычно знает хороший разработчик.
Сегодня я хочу поговорить про React и JSX. Почти в каждом проекте мы пишем JSX шаблоны, части которых рендерятся в зависимости от условий. Например, показывать ли комментарии или их вовсе нет? Делаем ли мы это правильно? Давайте разберемся. Это перевод оригинальной статьи.
Условный рендер в JSX. Советы
Условный рендер является краеугольным камнем в любом языке шаблонов. React / JSX смело пошли своим путем и решили не выделять для этого специальную синтаксическую конструкцию, как ng-if="condition", а положиться на булевы операторы в JavaScript :
condition && <JSX />рендерит<JSX />если условиеconditionвернетtruecondition ? <JsxTrue /> : <JsxFalse />рендерит<JsxTrue />или<JsxFalse>в зависимости от того, что вернет условиеcondition.
Это безусловно смелый, но не всегда интуитивно понятный, подход. Время от времени я сам стреляю себе в ногу, когда использую JSX условия. В этой статье я хочу рассмотреть неоднозначные моменты, которые присущи таким условиям, и дать советы как обезопасить себя:
Чтобы цифра
0внезапно не появлялась в версткеСоставные условия с
||могут удивить вас из-за приоритетаТернарные операторы не очень масштабируются
props.childrenне стоит использовать в условияхКак иметь дело с
updateиremountв условиях
Если вы сильно спешите, то я сделал шпаргалку:

Остерегайтесь нуля
Рендер на основе числового условия это обычная практика. Например, мы часто рендерим коллекции, только в том случае, когда они не пусты:
{gallery.length && <Gallery slides={gallery}>}
Но, если переменная gallery будет пустой, то мы получим 0 в нашем DOM, а не просто ничего. Это происходит из-за того как работает &&: неправдивое выражение стоящее слева (такое как 0) немедленно становится результатом выражения. В JavaScript булевы операторы не приводят собственный результат к булевым значениям. И это к лучшему, вы ведь не хотите, чтобы выражение справа было приведено к true. А React потом просто помещает этот 0 в DOM, в отличие от false, это корректный React элемент (опять же, это хорошо, например, в случае “у вас {count} билетов” мы хотим, чтобы был выведен 0).
Как это решить? У меня два варианта. Приведите выражение к булевому типу явно. Тогда значение выражения будет false, а не 0. А значение false не будет отображено:
gallery.length > 0 && jsx
// or
!!gallery.length && jsx
// or
Boolean(gallery.length) && jsx
Есть еще одно решение, замените && на тернарный оператор, чтобы явно определить значение в случае false. Работает отлично:
{gallery.length ? <Gallery slides={gallery} /> : null}
Следите за приоритетом
And (&&) имеет больший приоритет, чем ИЛИ (||) — так работает булева алгебра. Но, еще это значит, что вы должны быть очень осторожны с JSX условиями, которые содержат ||. Например, вы хотите отобразить ошибку доступа для анонимных пользователей или пользователей, которым вы ограничили доступ:
user.anonymous || user.restricted && <div className="error" />
...и это фиаско! Код выше, на самом деле, такой же как:
user.anonymous || (user.restricted && <div className="error" />)
А это не то, что вы хотели. Для анонимных пользователей у вас будет true || ...whatever..., что в результате даст true. Потому что JavaScript знает, что выражение OR будет true просто проанализировав левую часть и пропускает остальное. React не отображает true, а даже если бы и отображал, true это не сообщение об ошибке, которое вы ожидали.
Возьмите за правило — как только вы видите OR берите выражение в скобки:
{(user.anonymous || user.restricted) && <div className="error" />}
Или более хитрый вариант, используйте тернарный оператор внутри условия:
{user.registered ? user.restricted : user.rateLimited &&
<div className="error" />}
В этом случае скобки не нужны. Но лучше избегать тернарных операторов внутри условий, потому что такие выражения выглядят сложно. Признайтесь, сложно понять, что оно делает, просто взглянув на это выражение.
Не будьте заложниками тернарных операторов
Тернарный оператор это отличное решение для того, чтобы переключаться между двумя элементами JSX. Как только элементов становится больше, отсутствие else if () превращает вашу логику в кашу достаточно быстро:
{isEmoji
? <EmojiButton />
: isCoupon
? <CouponButton />
: isLoaded && <ShareButton />}
Любое дополнительное условие внутри тернарного оператора, будь то еще один тернарный оператор или &&, это красный флаг. Иногда, несколько && блоков выглядят лучше, вы платите за это повторением некоторых условий:
{isEmoji && <EmojiButton />}
{isCoupon && <CouponButton />}
{!isEmoji && !isCoupon && isLoaded && <ShareButton />}
В других случаях, старый добрый if/else это лучшее решение. Конечно, вы не можете поместить это в JSX, но вы можете вынести это в отдельную функцию:
const getButton = () => {
if (isEmoji) return <EmojiButton />;
if (isCoupon) return <CouponButton />;
return isLoaded ? <ShareButton /> : null;
};
Не стройте свои условия на JSX элементах
Что если я скажу, что React элементы, которые передаются через pros не стоит использовать как условия? Давайте попробуем обернуть children в div только, если children присутствует:
const Wrap = (props) => {
if (!props.children) return null;
return <div>{props.children}</div>
};
Я бы хотел, чтобы Wrap отрендерил null, когда контент, который нужно оборачивать, отсутствует. Но React работает подругому:
props.childrenможет быть пустым массивом, например,<Wrap>{[].map(e => <div />)}</Wrap>children.lengthтоже не подходит:childrenтакже может быть одним элементом, а не массивом (<Wrap><div /></Wrap>)React.Children.count(props.children)поддерживает и один и несколько элементов какchildren. Этот метод думает, что<Wrap>{false && 'hi'}{false && 'there'}</Wrap>содержит 2 элемента, хотя в действительности там ни одного.Теперь попробуем:
React.Children.toArray(props.children)удаляет недействительные элементы, такие какfalse. К сожалению, вы все равно получите пустой фрагмент:<Wrap><></></Wrap>.И последний гвоздь в крышку гроба, что если мы переместим условное отображение в компонент:
<Wrap><Div hide /></Wrap>сDiv = (p) => p.hide ? null : <div />? Мы никогда не можем знать, пустой ли он во время рендераWrap, потому чтоReactбудет отображать дочернийDivпосле родительского элемента. А дочерний элемент, у которого есть состояние, может быть повторно отрендерен независимо от родительского элемента.
Есть лишь один способ изменить что-то если интерполированый JSX пустой, посмотрите на :empty псведокласс в CSS.
Remount или update?
JSX который написан с использованием отдельных тернарных операторов выглядит как полностью независимый друг от друга код. Например:
{hasItem ? <Item id={1} /> : <Item id={2} />}
Что произойдет, если hasItems изменится? Не знаю как вы, а я предположу, что <Item id={1} /> демонтируется (unmounts), а потом <Item id={2} /> примонтируется (mounts), потому, что я написал 2 отдельных JSX тэга. React, в свою очередь, не знает о том что я там написал, все что он видит это элемент Item на одном и том же месте. Так что он сохраняет уже смонтированную сущность и обновляет ее свойства (пример в sandbox). Код, который вы видите выше, эквивалентен такому: <Item id={hasItem ? 1 : 2} />.
Когда ветка содержит разные компоненты, как в
{hasItem ? <Item1 /> : <Item2 />},Reactделает remount, потому чтоItem1не может быть обновлена, чтобы статьItem2.
Пример выше может сработать неожиданно. Это работает, пока вы правильно обрабатываете обновление элементов. Это даже немного лучше, чем перемонтирование (remounting). Но с неуправляемыми (uncontrolled) input вы можете попасть в беду:
{mode === 'name'
? <input placeholder="name" />
: <input placeholder="phone" />}
Здесь, если вы введете что-то в name input, а потом переключите mode, ваше имя, неожиданно, появится в phone input. Хотите проверить? Вот sandbox. Это может принести еще больше хаоса, если мы говорим о сложных механиках обновления (update), которые основываются на предыдущем состоянии.
Один из способов сделать это правильно это использовать свойство key. Обычно мы используем его для рендеринга списков, но, в принципе, это подсказка для React о том, что элемент уникальный. А вот элементы с одним и тем же key для React могут выглядеть одинаково.
// remounts on change
{mode === 'name'
? <input placeholder="name" key="name" />
: <input placeholder="phone" key="phone" />}
Другой вариант — заменить тернарный оператор на && блоки. Когда key отсутствует, React смотрит на индекс элемента в массиве children. Так что если мы поместим разные элементы на разные позции, это сработает так же хорошо, как и явное определение ключа:
{mode === 'name' && <input placeholder="name" />}
{mode !== 'name' && <input placeholder="phone" />}
И наоборот, если разные свойства элемента зависят от условия, то вы можете разделить этот элемент на два отдельных JSX тэга. Читать код так тоже будет проще, а недостатков никаких.
// messy
<Button
aria-busy={loading}
onClick={loading ? null : submit}
>
{loading ? <Spinner /> : 'submit'}
</Button>
// maybe try:
{loading
? <Button aria-busy><Spinner /></Button>
: <Button onClick={submit}>submit</Button>}
// or even
{loading && <Button key="submit" aria-busy><Spinner /></Button>}
{!loading && <Button key="submit" onClick={submit}>submit</Button>}
// ^^ bonus: _move_ the element around the markup, no remount
Итого
Резюмируем. Вот лучшие советы, о том как делать условия в JSX как про:
{number && <JSX />}отрендерит0а не ничего. Лучше делать{number > 0 && <JSX />}.Не забывайте про скобки вокруг или условий:
{(cond1 || cond2) && <JSX />}Тернарные операторы не очень подходят, если условий больше 2. Используйте
&&блоки для каждого условия. Или вынесите логику вif / elseВ не можете точно определить есть ли что-то в
props.children. Попробуйте использоватьCSS:empty.{condition ? <Tag props1 /> : <Tag props2 />}не приведет к тому, чтоTagбудет еще перемонтирован (remount). Используйте уникальныйkeyили разделите код на&&блоки.
А еще...
Здесь говорю опять я, Павел. В конце еще раз приглашу вас в свой Telegram-канал. На канале Хороший разработчик знает я минимум три раза в неделю простым языком рассказываю про свой опыт, хард скиллы и софт скиллы. Я 15+ лет в IT, мне есть чем поделиться. Все это нужно разработчику, чтобы делать свою работу хорошо, работать в удовольствие, быть востребованным на рынке и получать высокую компенсацию.
А для любителей картинок и историй есть ???? Instagram.
Спасибо ????
Комментарии (3)

thoughtspile
29.01.2022 13:59+1Привет! Пожалуйста, не нужно без разрешения публиковать плохие переводы моих статей, я планирую переводить их сам.

PavloPoliakov Автор
29.01.2022 14:04+1Привет, хорошо, больше не буду.
Я могу подредактировать, если какие-то конструкции бросаются вам в глаза, пишите. Написал в ПМ.
eeeMan
"Конечно, вы не можете поместить это в JSX" могу - самовызывающаяся функция внутри jsx допустима, а внутри функции я могу писать всё что есть в языке js. Учись сынок.