Добрый день, меня зовут Павел Поляков, я 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 условия. В этой статье я хочу рассмотреть неоднозначные моменты, которые присущи таким условиям, и дать советы как обезопасить себя:

  1. Чтобы цифра 0 внезапно не появлялась в верстке

  2. Составные условия с || могут удивить вас из-за приоритета

  3. Тернарные операторы не очень масштабируются

  4. props.children не стоит использовать в условиях

  5. Как иметь дело с 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)


  1. eeeMan
    28.01.2022 23:03
    +1

    "Конечно, вы не можете поместить это в JSX" могу - самовызывающаяся функция внутри jsx допустима, а внутри функции я могу писать всё что есть в языке js. Учись сынок.


  1. thoughtspile
    29.01.2022 13:59
    +1

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


    1. PavloPoliakov Автор
      29.01.2022 14:04
      +1

      Привет, хорошо, больше не буду.

      Я могу подредактировать, если какие-то конструкции бросаются вам в глаза, пишите. Написал в ПМ.