Давайте представим себе частый кейс - вы с бека получаете какой-то массив данных, из которых вы будете делать разметку, что будет содержимым вашей страницы.
Для примера возьмем список пользователей, и вам надо отрендерить этот список. Вам пришел массив и вы, не теряя времени, прогоняете его через 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)
Kuch
01.12.2023 20:49+7Прошу прощения, выскажу сугубо свое мнение. Это как-то слишком коротко и просто для статьи. И как заметили выше - если обобщить, то это просто разделение логики и верстки, что должно быть аксиомой и не является открытием. Более того, эта функция с проходом по массиву, подсчётами, псевдо проходами по массиву внутри прохода (to lowercase, includes) будет исполнятся на каждый rerender, хотя в этой трате ресурсов нет необходимости, ведь вы сказали, что данные просто пришли с бэка. Значит после того как они пришли с бэка нужно просто один раз пройтись и сформировать нужную структуру и отдать в этот компонент как пропс, более того убрав абсолютно ненужное тут усложнение в виде карирования. И если это все вынести в отдельный хук или файл например с названием utils/visualLogic, то код станет чище, понятнее для других разработчиков и более производительный.
BigDflz
01.12.2023 20:49-1С бэка приходит строка , фронт ее преобразует в элементы dom. Бэк стоит строку json, xml или html. Бэку по затратам по-барабану , что строить. Фронту же есть разница с чем работать, с json, xml или html. Для фронта самое оптимальное - html вставка в dom одна команда. Xcc? кто-то пробовал это сделать в современных браузерах? Результат приятно удивит. Да и есть уже инструменты... Теневой дом - та же работа по преобразованию входной строки в строку html . Бэки не умеют строить html? Пусть фронтеры дадут шаблон, чтоб его бэки заполнили. Или нафиг такие бэки. Серверный рендеринг? Хорошая идея загрузить сервер бесполезной работой по преобразованию из одной строки в другую..
motoroller95
01.12.2023 20:49да при таком подходе вообще от фронта отказаться можно, бэк умеет рендерить весь хтмл сам
rtatarinov
01.12.2023 20:49А теперь представь мультиязычность, плюрализацию и склонения. Эта вся свалка должна быть на бэке? А если тебе нужно в разных случаях разные форматы рисовать? Прям например на юзера. В одном случае ФиО, в другом фамилию и инициалы, в третьем имя и дату последнего посещения.
это даже концептуально звучит неправильно. Все что связано с отображением должно строиться на уровне клиента.
idd451289
01.12.2023 20:49Жсон юзают не потому что он быстрее хтмл, а потому что он удобнее, если говорить про спа. Во первых как минимум на этот кусок хтмла надо навесить события, надо будет вытянуть данные для того чтобы их можно было отредачить, чтобы их можно было куда засейвить, и прочее. И собсна разбираться как потом из хтмла вытянуть данные ради "оптимизации" не хочется. Плюс концепция реактивности(ну тут скорее даже не реактивности а стейтов будет нам мешать). Ну а как максимум апи нужно ещё и для других сред, например для мобильной аппы. Можно конечно дублировать генерацию хтмл и жсон, но мы опять же перетягиваем огромное количество лишнего кода на бэк
antytoto
01.12.2023 20:49Не могу понять, почему строки с ФИО остаются в нижнем регистре. Я понимаю, что условный пользователь может ввести все как зря, но почему потом имя не приводится к первой заглавной? Что делать с корнер-кейсами типа семитских или грузинских отчеств? Не имеет ли смысла запихнуть какую-то проверну не на этапе рендера, а на этапе ввода, и затем уже хранить правильные ФИО в базе, а тут не приводить их к нижнему регистру.
Во втором примере можно поправить опечатку retrun => return
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> );
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> ) }
Alexandroppolus
01.12.2023 20:49+5proxyUserVars
Это пример, как максимально усложнить код, нагромождая ФП на ровном месте.
Всё то же самое, только чуть по-другому (+фрагмент, забытый незаслуженно):
Код
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 для ЛистАйтема будет совсем не лишним.karmacan Автор
01.12.2023 20:49да в принципе можно и так, как вы написали, тоже хороший вариант ) я предложил вариант для тех, кто хочет упороться и съэкономить эти несчастные 4 строки и поупражняться с прокси. но в целом да, ваш вариант тоже хороший
qwr
01.12.2023 20:49{users.map(user => <User user={user}/>)}
В самом компоненте User будет разметка одного айтема, а логику вынесем в хук useUser.
belousovnikita92
01.12.2023 20:49Особого смысла делать это через коллбэк нет имхо, это может быть просто функция-конвертер, которую мы и так и так в компоненте используем, но будет легче дебажить и вообще разобраться, что и откуда пришло
Bono_houdini
01.12.2023 20:49Выносим список в <UsersList />, по-необходимости мемим преобразования внутри и успешно рисуем вью. Как было выше замечено плохая практика вызывать в ретурне на каждый рендер довольно сложную функцию. Декомпозиция вроде и была сделана, но при этом код по-прежнему афектит перформанс
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> ); };
Elendiar1
01.12.2023 20:49Разве не best practices вынести элемент списка отдельным компонентом, в который передавать юзера и внутри него делать всю логику?
BlackBorsh
01.12.2023 20:49Прошу прощения, я правильно понял, что в последнем листинге кода - ошибка, перепутаны местами аргументы вложенного коллбэка? вместо
user => callback =>
нужно callback => user =>
soll1992
01.12.2023 20:49Разве при таком вызове функции внутри map аргументы не поменяются местами?
users.map(proxyUserVars(callback))
Тоесть колбек зайдет при первом вызове функции, а элемент массива при втором. Тогда уж можно явно послать элемент вперед
users.map(user => proxyUserVars(user)(callback))
Ну или поменять порядок аргументов в прокси функции
Zenitchik
Или другими словами: разделяйте логику и отрисовку.
karmacan Автор
да, так лучше будет в заголовке, спасибо )
Kenya-West
Это точно не про React.