Добрый день, меня зовут Павел Поляков, я Principal Engineer
в каршеринг компании SHARE NOW, в Гамбурге в ???????? Германии. А еще я автор телеграм канала Хороший разработчик знает, где рассказываю обо всем, что обычно знает хороший разработчик.
Сегодня я хочу поговорить про React
и JSX
. Почти в каждом проекте мы пишем JSX
шаблоны, части которых рендерятся в зависимости от условий. Например, показывать ли комментарии или их вовсе нет? Делаем ли мы это правильно? Давайте разберемся. Это перевод оригинальной статьи.
Условный рендер в JSX. Советы
Условный рендер является краеугольным камнем в любом языке шаблонов. React
/ JSX
смело пошли своим путем и решили не выделять для этого специальную синтаксическую конструкцию, как ng-if="condition"
, а положиться на булевы операторы в JavaScript
:
condition && <JSX />
рендерит<JSX />
если условиеcondition
вернетtrue
condition ? <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. Учись сынок.