Я люблю сталкиваться с трудностями. Но с такими, которые можно решить, подумать над интересным решением, подобрать технологию. Люблю быть в потоке, а после решения чувствую себя настоящим профессионалом.
Но есть кое-что, из-за чего я не люблю программировать. Как ни странно, это тоже трудности, только другого рода. Например, когда, чтобы пофиксить баг, приходится разбираться с легаси-компонентом, который написан на классах на 300 строк кода. Разбираясь уже второй час, ловлю себя на мысли, что уже 10 минут просто смотрю в экран, а в голове «из-за угла» выглядывает мысль «Псс, парень, программирование — это не твое». Такие задачи не вызывают удовлетворения.
Если у вас есть компоненты с кучей условий, которые сложно читать, ревьюить и понимать, что там происходит, то эта статья для вас. Здесь я поделюсь подходом, который поможет уменьшить большие и страшные React-компоненты.

Примечание. Весь код, приведенный ниже, условный. В нём нет useEffect’ов, обработчиков, и прочего.
История жизни одной формы авторизации
Всё начиналось как обычно: стандартная форма авторизации, заголовок, два инпута с логином и паролем, и кнопка submit.
import React from ‘react’; import { Form, Input, Button, Title } from ‘our-design-system’; function AuthForm() { return ( <div> <Title>Войти в интернет-банк</Title> <Form> <Input placeholder=”Введите логин” type=”text”/> <Input placeholder=”Введите пароль” type=”password”/> <Button type=”submit”>Войти</Button> </Form> </div> ); } export default AuthForm;
Новые условия. Внезапно мы узнаем, что вообще-то нужно ещё авторизоваться по номеру карты или счёта. Запрос идёт в тоже место, заголовок тот же, кнопочка та же, но только вот нужно добавить «всего» 2 поля. Недолго думая, делаем что-то подобное.
function AuthForm({ authType, theme }) { const [accountType, setAccountType] = useState(‘account’); const changeAccountType = () => {setAccountType(‘card’)}; return ( <div> <Title theme={ theme }>Войти в интернет-банк</Title> <Form theme={ theme }> { authType === “login” ? (<div class=”login-form”> <Input theme={ theme } placeholder=”Введите логин” type=”text”/> <Input theme={ theme } placeholder=”Введите пароль” type=”password”/> </div>) : (<div class=”card-form”> { accountType === ‘card’ ? <Input theme={ theme } placeholder=”Введите номер карты” type=”number”/> : <Input theme={ theme } placeholder=”Введите номер счета” type=”number”/> } </div> } { authType === “account” && <Button theme={ theme } type=”button” onClick={changeAccountType} >Войти по { accountTypes [accountType] }</Button> } <Button theme={ theme } type=”submit”>Войти</Button> </Form> </div> ); }
Да, прямо в форму добавляем новый пропс (authType), который определяет тип аутентификации по логину-паролю или номеру карты/счёта. Внутри рендера делаем тернарник. Мы выбираем: будем рендерить поле логина-пароля или номера карты/счёта.
Внизу ещё есть кнопка, которая как раз переключает эти инпуты (она не нужна, если входим по логину).
Итого у нас появилось 2 новых условия в нашем компоненте.
Ещё новые условия. Дальше оказывается, что наша форма должна отображаться в мобильном приложении — пользователи приложения должны аутентифицироваться через нашу форму. В этом нет ничего особенного — просто не должен отображаться заголовок.
Сказано-сделано — добавляем еще один пропс isWebview, в котором мы проверяем: отображаем форму через вебвью или нет.
function AuthForm({ authType, theme, isWebview, }) { const [accountType, setAccountType] = useState(‘account’); const changeAccountType = () => {setAccountType(‘card’)}; return ( <div> { !isWebview && <Title theme={ theme }>Войти в интернет-банк</Title> <Form theme={ theme }> { { authType === “login” ? (<div class=”login-form”> <Input theme={ theme } placeholder=”Введите логин” type=”text”/> <Input theme={ theme } placeholder=”Введите пароль” type=”password”/> </div>) : (<div class=”card-form”> { accountType === ‘card’ ? <Input theme={ theme } placeholder=”Введите номер карты” type=”number”/> : <Input theme={ theme } placeholder=”Введите номер счета” type=”number”/> } </div> } { authType === “account” && <Button theme={ theme } type=”button” onClick={changeAccountType} >Войти по { accountTypes [accountType] }</Button> } <Button theme={ theme } type=”submit”>Войти</Button> </Form> </div> ); }
Также добавляем условие «Не показывать заголовок, если мы в мобильном приложении».
Редизайн. Проходит время и «случается» редизайн мобильного приложения. Естественно, нам тоже нужно обновляться. Это довольно простая доработка — меняем поля ввода карты или счета на одно поле. Соответственно, мы убираем кнопку, которая меняет эти поля местами при нажатии.
Замечательно, меньше полей — меньше проблем, меньше работы, верно?
Почти. Нюанс в том, что пользователи мобильных приложений не побегут дружно обновлять мобильное приложение: кто-то сидит через старую версию, кто-то через новую. Мы-то отображаем через вебвью — у нас всего одна версия, нам приходится поддерживать два разных варианта этой формы.
Что мы делаем? Правильно — добавляем еще один пропс на проверку дизайна (isNewDesignWebview), и ещё один вложенный тернарник.
function AuthForm({ authType, theme, isWebview, isNewDesignWebview }) { const [accountType, setAccountType] = useState(‘account’); const changeAccountType = () => {setAccountType(‘card’)}; return ( <div> { !isWebview && <Title theme={ theme }>Войти в интернет-банк</Title> <Form theme={ theme }> { isNewDesignWebview ? <CardInput theme={ theme } placeholder=‘’Введите номер карты или счета’’/> : authType === ‘’login’’ ? (<div class=”login-form”> <Input theme={ theme } placeholder=”Введите логин” type=”text”/> <Input theme={ theme } placeholder=”Введите пароль” type=”password”/> </div>) : (<div class=”card-form”> { accountType === ‘card’ ? <Input theme={ theme } placeholder=”Введите номер карты” type=”number”/> : <Input theme={ theme } placeholder=”Введите номер счета” type=”number”/> } </div> } { authType === “account” && !isNewDesignWebview && <Button theme={ theme } type=”button” onClick={changeAccountType} >Войти по { accountTypes [accountType] }</Button> } <Button theme={ theme } type=”submit”>Войти</Button> </Form> </div> ); }
Естественно, внизу ещё одно условие, что для нового дизайна кнопка нам не нужна.
Итого. У нас есть форма: без логики, просто рендер, 3 условных пропса, по которым мы определяем, что конкретно будем рендерить, в тех или иных случаях, и 7 (новых) условий.
Кажется, что всё очень-очень плохо. Мы кричим в монитор, что не хотим это всё поддерживать, и идём в интернет, чтобы найти решение проблемы.
Но никуда идти не надо, у меня для вас уже есть одно решение.
Compound components
Небольшая вводная. Наверняка вы знаете, как выглядят селекты (<select>) в HTML.
<select name=”Office”> <option value=”Dwight”>Schrute</option> <option value=”Micheal”>Scott</option> <option value=”Jim”>Halpert</option> <option value=”Pam”>Beesly</option> </select>
Это какая-то сущность, которая наполняется опшенами (<options>). При этом опшены не могут существовать вне селекта. По отдельности селекты и опшены бесполезны, а вместе работают как составные компоненты, создавая единую логику.
Compound components использует подобную систему: нельзя использовать элементы Compound components вне основного большого компонента. В этом подходе мы объединяем несколько компонентов общей сущностью и общим состоянием. Отдельно от этой сущности их использовать нельзя — они единое целое.
Немного забегая вперёд, покажу как выглядит наша форма аутентификации, если мы применим к ней этот подход.
export default function LoginAuth( ) { return ( <AuthForm theme={ ‘dark’}> <AuthForm.AuthTitle/> <AuthForm.LoginInput/> <AuthForm.PasswordInput/> <AuthForm.SubmitButton/> </AuthForm}> ) }
У нас есть форма с элементами. Вне формы элементы не могут существовать. В самой форме зашита логика, которая передается каждому элементу.
Примечание. Может смутить то, что мы вызываем наши элементы через точку, но это такой синтаксис.
Подход Compound components похож на методологию BEM.
-
У нас есть блок — форма аутентификации;
-
есть элементы — заголовок, инпуты;
-
а модификаторы — это пропсы;
-
у самих элементов тоже могут быть какие-то пропсы как модификаторы.
Интересно то, что мы можем использовать элементы в разных ситуациях.
Переписываем форму с помощью Compound components
Давайте перепишем наш компонент и на его примере покажу как Compound components работает.
import { CardAccount, AuthCardInput, LoginInput, PasswordInput, SubmitButton, AuthTittle } from ‘./components’; const AuthFormContext = React,createContext(undefined); function AuthForm(props) { const { theme } = props; const memoizedContextValue = React.useMemo{ ( ) => ({ theme }), [theme], ); return ( <AuthFormContext.Provider value={ memoizedContextValue }> <Form onSubmit={ submitForm } > { props.children } </Form> </AuthFormContext.Provider> ); } export function useAuthContext( ) { const context = React.useContext(AuthFormContext); if ( !context) { throw new Error(‘This component must be used within a <AuthForm> component.’); } return context; } AuthForm.AuthTitle = AuthTitle; AuthForm.LoginInput = LoginInput; AuthForm.PasswordInput = PasswordInput; AuthForm.CardAccount = CardAccount; AuthForm.AuthCardInput = AuthCardInput; AuthForm.SubmitButton = SubmitButton;
Разберем по частям.
Для общей логики мы используем контекст, и можем передавать актуальные данные в форму на любой уровень вложенности. Мы создаем контекст, но его не экспортим.
const AuthFormContext = React,createContext(undefined);
Здесь мы создаем наши данные для всех элементов и мемоизируем их. Естественно, сами элементы тоже нужно обернуть будет в мемо, чтобы мемоизация работала.
function AuthForm(props) { const { theme } = props; const memoizedContextValue = React.useMemo{ ( ) => ({ theme }), [theme], );
Пробрасываем контекст в нашу форму.
return ( <AuthFormContext.Provider value={ memoizedContextValue }> <Form onSubmit={ submitForm } > { props.children } </Form> </AuthFormContext.Provider> );
Здесь защита от дурака.
if ( !context) { throw new Error(‘This component must be used within a <AuthForm> component.’); }
С помощью неё мы не сможем использовать наши элементы вне формы (да и не надо). В каждом элементе зашита какая-то бизнес-логика, которую мы не хотим выдирать из этого компонента.
Если мы хотим переиспользовать отдельно внутренние элементы нашего сложного компонента (поля, кнопки и т.п.), то Compound Components нам не подходит.
В этой же «защите» создаём кастомный хук, в котором вызываем наш контекст и проверяем его наличие. Если контекста нет — выбрасываем ошибку. Это значит, что кто-то попытался использовать элемент вне формы.
Последнее — записываем в статические свойства все наши элементы.
AuthForm.AuthTitle = AuthTitle; AuthForm.LoginInput = LoginInput; AuthForm.PasswordInput = PasswordInput; AuthForm.CardAccount = CardAccount; AuthForm.AuthCardInput = AuthCardInput; AuthForm.SubmitButton = SubmitButton;
Как это используется?
Вот наша форма авторизации по логину и паролю с заголовком.
<AuthForm theme={ ‘dark’ }> <AuthForm.AuthTitle/> <AuthForm.LoginInput/> <AuthForm.PasswordInput/> <AuthForm.SubmitButton/> </AuthForm}>
Вот форма авторизации уже по карте или счету.
<AuthForm theme={ ‘dark’ }> <AuthForm.AuthTitle/> <AuthForm.CardAccount/> <AuthForm.SubmitButton/> </AuthForm}>
Это форма для пользователей мобильных приложений со старым дизайном.
<AuthForm theme={ ‘dark’ }> <AuthForm.AuthCardInput/> <AuthForm.SubmitButton/> </AuthForm}>
А это форма для пользователей с новым дизайном.
<AuthForm theme={ ‘dark’ }> <AuthForm.CardAccount/> <AuthForm.SubmitButton/> </AuthForm}>
Мы вынесли логику условий из компонента (рендера) на уровень выше, туда, где мы используем этот компонент. Мне кажется такой вариант нагляднее: нам не нужно лезть в компонент, чтобы понять какие нам нужно пропсы прокинуть в компонент, чтобы отобразился заголовок и т.д. Мы всё выбираем сами.
Сравним рендеры. Оценим масштаб: как рендер выглядел раньше с множеством разных условий, и как он выглядит сейчас.
Как используется обычный компонент:
<AuthForm theme="dark" authType="account" isWebview={true} isNewDesignWebview={true} />
Его рендер:
return ( <div> { !isWebview && <Title theme={ theme }>Войти в интернет-банк</Title> } <Form theme={ theme }> { isNewDesignWebview ? <CardInput theme={ theme } placeholder="Введите номер карты или счета"/> : authType === "login" ? (<div class="login-form"> <Input theme={ theme } placeholder="Введите логин" type="text"/> <Input theme={ theme } placeholder="Введите пароль" type="password"/> </div>) : (<div class="card-form"> { accountType === 'card' ? <Input theme={ theme } placeholder="Введите номер карты" type="number"/> : <Input theme={ theme } placeholder="Введите номер счета" type="number"/> } </div>) } { authType === "account" && !isNewDesignWebview && <Button theme={ theme } type="button" onClick={changeAccountType} >Войти по { accountTypes[accountType] }</Button> } <Button theme={ theme } type="submit">Войти</Button> </Form> </div> );
Теперь как используется Compound Component:
<AuthForm theme={'dark'}> <AuthForm.AuthCardInput/> <AuthForm.SubmitButton/> </AuthForm>
И его рендер:
return ( <AuthFormContext.Provider value={ memoizedContextValue }> <Form onSubmit={ submitForm } > { props.children } </Form> </AuthFormContext.Provider> );
А это вынесенные элементы (здесь только те элементы, что используются в примере, но в других примерно тоже самое, потому что в нашей форме нет никакой бизнес логики):
export default function AuthCardInput() { const { theme } = useAuthContext(); return ( <CardInput theme={ theme } placeholder="Введите номер карты или счета"/> ) } export default function SubmitButton() { const { theme, submitForm } = useAuthContext(); return ( <Button theme={ theme } type="submit" onClick={ submitForm }> Войти </Button> ) }
Когда стоит использовать Compound components?
-
Когда вы хотите объединить несколько компонентов (элементов) в одну сущность. Это могут быть не простые селекты с опшенами, а, например, компонент с табуляцией.
-
Когда мы видим, что рендер становится перегружен из-за множества пропсов. Это как раз ситуация, как у нас с формой, в которой много пропсов. При этом в самом рендере много условий отображения компонента.
Статья подготовлена на основе выступления на online-конференции HolyJS. Запись выступления доступна в группе Alfa Digital в ВК, там также есть записи докладов с других конференций и митапов. Также подписывайтесь на Телеграм-канал Alfa Digital — там мы постим новости, опросы, видео с митапов, иногда шутим.
Рекомендуем почитать.
-
Неподатливые soft-skills: почему нам всё ещё нужен эмоциональный интеллект
-
Data Science Meet Up #2: LTV, Uplift, совершенство и Reject/Inference
-
Как снимать логи с устройств на Android и iOS: разбираемся с инструментами
-
Как мы переходили на React-router v6: подводные камни и альтернативы
-
Как и зачем мы начали искать бизнес-инсайты в отзывах клиентов с помощью машинного обучения
ссылка на оригинал статьи https://habr.com/ru/company/alfa/blog/691976/
Добавить комментарий