Давайте представим себе частый кейс - вы с бека получаете какой-то массив данных, из которых вы будете делать разметку, что будет содержимым вашей страницы.

Для примера возьмем список пользователей, и вам надо отрендерить этот список. Вам пришел массив и вы, не теряя времени, прогоняете его через map, создавая, таким образом свою разметку - вот так:

return (
  <ul className="list">
    {users.map(user => (
      <Fragment key={user.name.name}>
        <li>{user.name.name} {user.name.patronomic} {user.name.surname}</li>
        <li>{user.regestrationDate}</li>
        <li>{user.status.description}</li>
      </Fragment>
    ))}
  </ul>
);

Окей, тут все просто. Но давайте представим, что с бека (как это часто бывает) данные вам приходят не чистыми, но их еще надо обрабатывать. Например, отчества может не быть, то есть нужно сделать проверку, а имена и фамилии нужно привести к ловеркейсу.

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

Ну и напоследок, если статус нужно залить соответствующим стилем из заготовленного словаря, который завязан на отдельном свойстве value; если стиля в словаре нет - сделать какой-нибудь дефолтный.

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

Давайте представим как это могло бы выглядеть:

return (
  <ul className="list">
    {users.map(user => {
      const fullName = `
        {user.name.name} 
        {user.name.patronomic ? user.name.patronomic : ''} 
        {user.name.surname}
      `.toLowerCase();
      const date = user.regestrationDate 
        ? moment(user.regestrationDate)
        : null;
      const statusColor = ['В сети', 'Не в сети'].includes(
        user.status.description,
      )
      ? STATUS_COLORS[user.status.value]
      : 'neutral';

      retrun (
        <Fragment key={user.name.name}>
          <li>{fullName}</li>
          <li>{date}</li>
          <li className={statusColor}>{user.status.description}</li>
        </Fragment>
      );
    })}
  </ul>
);

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

Что тут можно сделать? Я предлагаю выносить все утилитарные вычисления в отдельную функцию с префиксом "with" или "proxy", которая будет как некая "прослойка" содержать код расчета переменных и передавать их обратно в колбек, который занимается разметкой.

Продемонстрируем. Создадим функцию "proxyUserVars", в которую обернем колбек разметки, перенесем в нее все переменные и передадим их обратно в колбек разметки с оригинальным обьектом user. Вот так:

const proxyUserVars = user => callback => {
  const fullName = `
    {user.name.name} 
    {user.name.patronomic ? user.name.patronomic : ''} 
    {user.name.surname}
  `.toLowerCase();
  const date = user.regestrationDate 
    ? moment(user.regestrationDate)
    : null;
  const statusColor = ['В сети', 'Не в сети'].includes(
    user.status.description,
  )
  ? STATUS_COLORS[user.status.value]
  : 'neutral';

  return callback(user, fullName, date, statusColor); // vars: fullName, date, statusColor
}

return (
  <ul className="list">
    {users.map(
      proxyUserVars((user, fullName, date, statusColor) => (
        <Fragment key={user.name.name}>
          <li>{fullName}</li>
          <li>{date}</li>
          <li className={statusColor}>{user.status.description}</li>
        </Fragment>
      ))
    )}
  </ul>
);

Обратите внимание как объявляется функция proxyUserVars - это множественный колбек. Чтобы не запутаться - первая переменная - это то что мы получаем вне proxyUserVars из map-а; вторая переменная - то что мы передаем в proxyUserVars, то есть наш колбек, который генерит разметку.

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

Вот и все. Держите свою разметку в форме - пожалейте глаза и время коллег.

Спасибо.

Комментарии (22)


  1. Zenitchik
    01.12.2023 20:49
    +3

    Или другими словами: разделяйте логику и отрисовку.


    1. karmacan Автор
      01.12.2023 20:49

      да, так лучше будет в заголовке, спасибо )


    1. Kenya-West
      01.12.2023 20:49
      -1

      разделяйте логику и отрисовку

      Это точно не про React.


  1. Kuch
    01.12.2023 20:49
    +7

    Прошу прощения, выскажу сугубо свое мнение. Это как-то слишком коротко и просто для статьи. И как заметили выше - если обобщить, то это просто разделение логики и верстки, что должно быть аксиомой и не является открытием. Более того, эта функция с проходом по массиву, подсчётами, псевдо проходами по массиву внутри прохода (to lowercase, includes) будет исполнятся на каждый rerender, хотя в этой трате ресурсов нет необходимости, ведь вы сказали, что данные просто пришли с бэка. Значит после того как они пришли с бэка нужно просто один раз пройтись и сформировать нужную структуру и отдать в этот компонент как пропс, более того убрав абсолютно ненужное тут усложнение в виде карирования. И если это все вынести в отдельный хук или файл например с названием utils/visualLogic, то код станет чище, понятнее для других разработчиков и более производительный.


    1. Alisher_Aituarov
      01.12.2023 20:49

      а что еще можно рассказать про эту простую и неглубокую тему ?


  1. BigDflz
    01.12.2023 20:49
    -1

    С бэка приходит строка , фронт ее преобразует в элементы dom. Бэк стоит строку json, xml или html. Бэку по затратам по-барабану , что строить. Фронту же есть разница с чем работать, с json, xml или html. Для фронта самое оптимальное - html вставка в dom одна команда. Xcc? кто-то пробовал это сделать в современных браузерах? Результат приятно удивит. Да и есть уже инструменты... Теневой дом - та же работа по преобразованию входной строки в строку html . Бэки не умеют строить html? Пусть фронтеры дадут шаблон, чтоб его бэки заполнили. Или нафиг такие бэки. Серверный рендеринг? Хорошая идея загрузить сервер бесполезной работой по преобразованию из одной строки в другую..


    1. motoroller95
      01.12.2023 20:49

      да при таком подходе вообще от фронта отказаться можно, бэк умеет рендерить весь хтмл сам


    1. rtatarinov
      01.12.2023 20:49

      А теперь представь мультиязычность, плюрализацию и склонения. Эта вся свалка должна быть на бэке? А если тебе нужно в разных случаях разные форматы рисовать? Прям например на юзера. В одном случае ФиО, в другом фамилию и инициалы, в третьем имя и дату последнего посещения.

      это даже концептуально звучит неправильно. Все что связано с отображением должно строиться на уровне клиента.


    1. idd451289
      01.12.2023 20:49

      Жсон юзают не потому что он быстрее хтмл, а потому что он удобнее, если говорить про спа. Во первых как минимум на этот кусок хтмла надо навесить события, надо будет вытянуть данные для того чтобы их можно было отредачить, чтобы их можно было куда засейвить, и прочее. И собсна разбираться как потом из хтмла вытянуть данные ради "оптимизации" не хочется. Плюс концепция реактивности(ну тут скорее даже не реактивности а стейтов будет нам мешать). Ну а как максимум апи нужно ещё и для других сред, например для мобильной аппы. Можно конечно дублировать генерацию хтмл и жсон, но мы опять же перетягиваем огромное количество лишнего кода на бэк


  1. antytoto
    01.12.2023 20:49

    Не могу понять, почему строки с ФИО остаются в нижнем регистре. Я понимаю, что условный пользователь может ввести все как зря, но почему потом имя не приводится к первой заглавной? Что делать с корнер-кейсами типа семитских или грузинских отчеств? Не имеет ли смысла запихнуть какую-то проверну не на этапе рендера, а на этапе ввода, и затем уже хранить правильные ФИО в базе, а тут не приводить их к нижнему регистру.

    Во втором примере можно поправить опечатку retrun => return


  1. edtech
    01.12.2023 20:49

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

      const processedUsers = useMemo(() => preprocessUserData(users), [users]);
    
      return (
        <ul className="list">
          {processedUsers.map((user) => (
            <UserListItem key={user.id} user={user} />
          ))}
        </ul>
      );
    


  1. easymikey
    01.12.2023 20:49

    А почему вы не использовали паттерн RenderProps ?
    C его помощью можно гибко резделять логику и отображение. И отдельно тестировать доменную и тестировать логику будет намного проще.

    type User = {
      name: string;
      // другие пропсы
    };
    // Чистая функция, можно протестировать
    function composeUser<T extends User>(
      user: T,
    ): { fullName: string; date: string; statusColor: string } {
        const fullName = `
        ${user.name.name}
        ${user.name.patronomic ? user.name.patronomic : ''}
        ${user.name.surname}
      `.toLowerCase();
        const date = user.regestrationDate ? moment(user.regestrationDate) : null;
        const statusColor = ["В сети", "Не в сети"].includes(user.status.description)
          ? STATUS_COLORS[user.status.value]
          : "neutral";
    
      return { fullName, date, statusColor };
    }
    
    const Users: FC<{ data: User[]; renderFn: (user: User) => void }> = ({
      data,
      renderFn,
    }) => {
      return <>{data.map(renderFn)}</>;
    };
    
    const App: FC = () => {
      /// откуда то получаем users
    
      return (
          <ul className="list">
            <Users
              data={[{ name: "username1" }, { name: "username2" }]}
              renderFn={(c) => {
                const { fullName, date } = composeUser(c);
                return (
                  <>
                    <li>{fullName}</li>
                    <li>{date}</li>
                  </>
                );
              }}
            />
        </ul>
      )
    }


  1. Alexandroppolus
    01.12.2023 20:49
    +5

    proxyUserVars

    Это пример, как максимально усложнить код, нагромождая ФП на ровном месте.

    Всё то же самое, только чуть по-другому (+фрагмент, забытый незаслуженно):

    Код
    const getUserVars = (user) => ({
      fullName: `
        {user.name.name} 
        {user.name.patronomic ? user.name.patronomic : ''} 
        {user.name.surname}
      `.toLowerCase(),
      
      date: user.regestrationDate 
        ? moment(user.regestrationDate)
        : null,
      
      statusColor: ['В сети', 'Не в сети'].includes(
        user.status.description,
      )
        ? STATUS_COLORS[user.status.value]
        : 'neutral',
    });
    
    return (
      <ul className="list">
        {users.map((user) => {
          const {fullName, date, statusColor} = getUserVars(user);
      
          return (
            <Fragment key={user.id}>
              <li>{fullName}</li>
              <li>{date}</li>
              <li className={statusColor}>{user.status.description}</li>
            </Fragment>
          );
        ))}
      </ul>
    );

    Вообще, поддержу мысль @edtech, что правильнее делать отдельный UserListItem . А если список может меняться, то React.memo для ЛистАйтема будет совсем не лишним.


    1. karmacan Автор
      01.12.2023 20:49

      да в принципе можно и так, как вы написали, тоже хороший вариант ) я предложил вариант для тех, кто хочет упороться и съэкономить эти несчастные 4 строки и поупражняться с прокси. но в целом да, ваш вариант тоже хороший


  1. qwr
    01.12.2023 20:49

    {users.map(user => <User user={user}/>)}

    В самом компоненте User будет разметка одного айтема, а логику вынесем в хук useUser.


  1. belousovnikita92
    01.12.2023 20:49

    Особого смысла делать это через коллбэк нет имхо, это может быть просто функция-конвертер, которую мы и так и так в компоненте используем, но будет легче дебажить и вообще разобраться, что и откуда пришло


  1. Bono_houdini
    01.12.2023 20:49

    Выносим список в <UsersList />, по-необходимости мемим преобразования внутри и успешно рисуем вью. Как было выше замечено плохая практика вызывать в ретурне на каждый рендер довольно сложную функцию. Декомпозиция вроде и была сделана, но при этом код по-прежнему афектит перформанс


  1. SanoLitch
    01.12.2023 20:49

    // list.tsx
    export const List = ({ className, children }): JSX.Element => (
      <ul className={ className }>
        { children }
      </ul>
    );
    
    // list-item.tsx
    export const ListItem = ({ user }): JSX.Element => (
      <li className={ className }>
        { children }
      </li>
    );
    
    // fullname.tsx
    export const FullName = ({ surname, name, patronymic }): JSX.Element => {
      const fullName = `
        {name} 
        {patronymic ?? ''} 
        {surname}
      `.toLowerCase();
      
      return (
        <span>
          { fullName }
        </span>
      );
    };
    
    // date.tsx
    export const Date = ({ date }): JSX.Element | null => {
      if (!date) {
        return null;
      }
      
      return (
        <span>
          { moment(date) }
        </span>
      );
    };
    
    // status.tsx
    import styles from './status.modules';
    
    enum ColorByStatus = {
      'В сети': styles.statusOnline, 
      'Не в сети': styles.statusOffline,
    };
    
    export const Status = ({ description }): JSX.Element | null => (
      <span className={ ColorByStatus[description] ?? styles.statusDefault }>
        { description }
      </span>
    );
    
    // user-list.tsx
    export const UserList = (users): JSX.Elemet => {
      return (
        <List className="user-list">
          {
            users.map(({ surname, name, patronymic, regestrationDate, status }) => (
              <FullName
                surname={ surname }
                name={ name }
                patronymic={ patronymic }
              />
              <Date date={ regestrationDate } />
              <Status description={ status.description } />
            )) 
          }
        </List>
      );  
    };
    
    
    
    
    
    
    
    


  1. Elendiar1
    01.12.2023 20:49

    Разве не best practices вынести элемент списка отдельным компонентом, в который передавать юзера и внутри него делать всю логику?


  1. g5-freemen
    01.12.2023 20:49

    Почему в .map нет React.Fragment key=


  1. BlackBorsh
    01.12.2023 20:49

    Прошу прощения, я правильно понял, что в последнем листинге кода - ошибка, перепутаны местами аргументы вложенного коллбэка? вместо user => callback => нужно callback => user =>


  1. soll1992
    01.12.2023 20:49

    Разве при таком вызове функции внутри map аргументы не поменяются местами?

    users.map(proxyUserVars(callback))

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

    users.map(user => proxyUserVars(user)(callback))

    Ну или поменять порядок аргументов в прокси функции