Comments 20
Всегда не понимал зачем в одном компоненте делают два совершенно разных: dropdown и multidropdown. Они же совершенно по разному себя ведут, принимают разные данные на входе и при этом наврядли надо будет на форме переключать из мультиселекта в синглеслект.
В этой статье не критично. Тут полезные приемы описаны чтобы это реализовать, но зачем это в реальности делать - не понятно.
Я с этой дилеммой сталкивался на своем проекте и решил всё таки делать объединенный dropdown. Этого позволяет избежать дублирования кода, так как общих элементов у них очень много.
У Реакта и особенно StyledComponet всё очень плохо с декомпозицией. Там сложно сделать обобщённое, но легко кастомизируемое решение, не наворотив комбайн. Да даже тут в статье просто горы кода для такой, казалось бы, простой задачи.
onMouseEnter для подсветки элементов, серьезно? А потом удивляемся, почему веб тормозит...
props.onChange(props.value.filter((value) => value !== item))
не пробовали использовать Set?
Предлагаете передавать в props.onChange каждый раз один и тот же объект? Это шаг к тому, чтобы запутаться в ссылках на объекты...
Если уж оптимизировать в эту сторону — надо начать с создания ObservableArray/ObservableSet, чтобы мутабельный объект и подписка на его изменения стали едины.
У меня больше притензии к производительности. На каждый чих мутируем массив, каждый елемент меню слушает onMouseEnter, зачем-то создается портал, хотя в простейшем случае можно без него. А с ним получается вот так, если не отслеживать размер окна.
Кто-то посмотрит, сделает также, один-два компонента норм, но после десятка таких, все будет тормозить. И виноват в этом будет JS и React.
Ну, про onMouseEnter я и не спорил, это перебор, особенно уж на каждом элементе.
А портал как раз понятно почему делался, и про это написано. Не знаю какой вы там простейший случай имели в виду, но в общем случае не сможете вы выпадающий список вставить в разметку так, чтобы никакая комбинация родительских элементов его не покорёжила.
По-моему опыту 90% случает хватает простейшего лейаута
<DropdownContainer> - position: relative
<DropdownList> - position: absolute
{content}
</DropdownList>
</DropdownContainer>
Но если нужен портал, надо понимать, что придется отслеживать коррдинаты и размеры родителя, а то случиться конфуз
А в оставшихся 10% случаях что делать? :)
Самое рабочее решение - это портал, который работает в 100% случаев.
И расчитывать координаты в момент отображения.
И прятать в момент ресайза/скрола - иначе прийдется вдаваться в филосовские вопросы кто же должен быть выше - заголовок, сайдбар меню или выпадающий список.
Вы изначально учите плохому. Если у вас хватает наглости сделать кастомный контрол, то и должно хватить энтузиазма, чтобы сделать компонент доступным.
А потом в проекте 3 версии дропдауна))
Используйте уже готовые компоненты, а еще лучше библиотеки компонентов и не нужно будет придумывать велосипед.
Вот как раз с библиотеками компонентов тут наоборот.
Если используется библиотечный компонент, то как правило он что-то да не умеет из того что нужно именно в этом проекте, и тогда наварачиваются костыли поверх него или находится другая библиотека но старый компонент не поменяешь, потому как пропсы разные - в итоге в проекте два (и более) одинаковых компонентов.
Свой же велосипед можно модифицировать и наварачивать сколь угодно долго не меняя при этом интерфейс его использования.
Основной же недостаток библиотек
1. то что они универсальны - добавляет тормознутости. Как правило в проекте нужна небольшая часть от всего функционала готового компонента.
2. то что они уже содержат стили - добавляет ненужный код в проект. Как правило в конкретном проекте нужны свои стили и стили библиотечного компонента просто напросто висят мертвым грузом.
Но я не за то чтобы писать все с нуля. Есть библиотеки из которых можно собрать компонент минимальными усилиями.
Типизация Dropdown
мне кажется у вас слишком усложнено, у вас такой компонент, что приходится делать так
Придется явно указывать тип, как в вашем примере
<Dropdown<Item> label='Dropdown' onChange={item => console.log(item)}>
Но можно написать например так, и тут не придется мапить items, так как маппер передается и плюс тс сам поймет какие вы данные передали и высчитает тип
на мой взгляд так будет удобнее работать, но не претендую на самое правильное решение
это чисто мое ИМХО
Забыл написать, в вашем случае дженерики вообще не нужны так как можно просто сделать так
указать тип в onChange
Я бы не советал так делать в переиспользуемом компоненте, потому что вечно вылезают всякие дизайнерские выверты. Например нужно добавить полу для поиска, сгруппировать некоторые айтемы, добавить на каждую подгруппу заголовок, добавить разделители, на некоторые пункты меню добавить иконки, раскрасить, disabled состояние, вложенные дропдауны, меню айтем с подтверждением через диалог и т.д.
Всё это не предусмотришь в базовом компоненте, если не превратить его в монструозный комбаин, поэтомы лучше прокидывать через children.
useEffect(() => {
if (isOpen) { document.addEventListener('keydown', handleKeyDown, true); }
return () => document.removeEventListener('keydown', handleKeyDown, true);
}, [isOpen]);
Если был перерендер при открытом дропдауне (например, что-то догрузилось), handleKeyDown будет устаревшим, то есть в его замыкании будет уже неактуальный набор items и indexes. Кейс относительно редкий, но по "закону Мерфи" его вероятность равна 100%. В зависимости эффекта надо бы добавить handleKeyDown, от греха. А саму функцию - в useCallback, чтобы не дергать эффект слишком часто (хотя это необязательно).
А вообще, зашивать сюда работу с "итемами" неправильно. Дропдавн задуман в общем виде, с передачей произвольного children в выпадушку. Значит он не должен соваться в children. Его задача - обеспечить правильную работу портальной выпадушки. А далее его можно будет скомбинировать с календарем/селектом/etc, и получить несколько выпадушечных компонентов на одном переиспользованном механизме.
Создание кастомизируемого Dropdown для React на TypeScript