Как на примере одной кнопки можно улучшить Frontend часть проекта

от автора

Всем привет! Я джуниор фронтенд разработчик. И хотел бы рассказать как иногда применение библиотек в проекте — это излишества, которые стоит избегать.

Давай начнем по порядку. Проект написан на NextJs, TS, TailwindCSS. И есть на сайте анимационная кнопка, которые при скролле красиво появляется и при клике открывает модалку.

Вся логика, запилена на Gsap. И казалось бы все хорошо, библиотека делает за нас дело, рисует анимации, а мы спокойно пьем чаек и отправляем ПР в гитхаб.

НО. Проблема была в том, что иногда анимация подвисала, дергалась, а на слабых ПК вообще отказывалась работать.

В итоге решился взяться за анимационную кнопку и посмотреть что же там ее кишках 🙂
И, о ЧУДО! Если посмотреть на компонент, то мне кажется он очень страшным. Как вам кажется?

export const AnimatedButtonLinkWitmUtm: React.FC<IAnimatedButtonProps> = ({   isDark = false,   btnColor = '#0A85D1',   btnShadow = '0 0 0 0px rgb(157,52,218)',   maxWidth = 315, }) => {   const buttonRef = useRef<HTMLDivElement>();   const tl = gsap.timeline();    useEffect(() => {     if (buttonRef.current) {       const trigger = ScrollTrigger.create({         trigger: buttonRef.current,         start: `top bottom-=400px`,         onEnter: () => {           tl.to(buttonRef.current, { opacity: 1, duration: 0.3 })             .to(buttonRef.current, {               duration: 0.3,               boxShadow: btnShadow,               ease: 'circ.in',             })             .to(buttonRef.current, {               duration: 0.3,               boxShadow: btnShadow,               ease: 'circ.out',             })             .to(buttonRef.current, {               maxWidth: `${maxWidth}px`,               width: '100%',               paddingLeft: 24,               duration: 1,             })             .to(buttonRef.current.children[1], { opacity: 1, duration: 2 }, '<0.4')             .to(buttonRef.current.children[0], { opacity: 1, duration: 2 }, '<0.5');         },         onLeaveBack: () => {           tl.to(buttonRef.current, { opacity: 0 })             .to(buttonRef.current.children[1], { opacity: 0 })             .to(buttonRef.current, { width: '55px', paddingLeft: 10 })             .to(buttonRef.current.children[0], { opacity: 0, delay: 0, duration: 0 });         },       });        return () => {         trigger.kill();       };     }   }, [btnShadow, maxWidth, tl]);    return (     <>       <div         ref={buttonRef}         className={cn(           'relative flex h-[56px] w-[56px] items-center justify-between gap-[16px] rounded-[10px] px-[10px] py-[8px] opacity-0 shadow-[inset_0px_1px_0px_0px_rgba(0,0,0,0.11)] backdrop-blur-[3.5px]',           {             'bg-[#D8D8D8]/30': !isDark,             'bg-[#424245]/70': isDark,           }         )}       >         <LinkWithUtm           href="https://a6b9d8dc-b142-4b92-b1d0-dfbfd2230471.selstorage.ru/assets/edu-program.pdf"           target="_blank"           className={cn(             'm-0 text-[17px] font-medium leading-[27.2px] opacity-0 after:absolute after:inset-0',             {               'text-black': !isDark,               'text-[#F5F5F7]': isDark,             }           )}         >           Подробнее о программе         </LinkWithUtm>         <div           className="h-[40px] w-[40px] min-w-[40px] rounded-[10px] p-[8px] opacity-0"           style={{ backgroundColor: btnColor }}         >           <Image src={arrowRight} alt="" />         </div>       </div>     </>   ); }; 

У нас есть логика на GSAP и триггер на каком моменте скрола она должна появляться.
Если посмотреть на анимацию, она основана на размерах и opacity.

А нужен ли нам GSAP для такой простой анимации??? НЕТ. И давайте я постараюсь объяснить почему.
1 — Вес библиотеки. Ради простейшей анимации вы подтягиваете в проект библиотеку весом 4 мб.
2 — Эту анимацию можно сделать на чистом CSS

Анимацию мы напишем, но для триггера, чтобы все срабатывало нам нужен лишь Observer. Все это можно реализовать с помощью хуков, но для такой ситуации есть библиотека намного легче и проще в использование и под капотом у нее лежат реактовские хуки.

React-Intersection-Observer

React-Intersection-Observer

Как вы видите пакет намного легче и популярнее по скачиваниям, так как он перекрывает простые потребности по триггерам и анимациям 🙂

И что в итоге у меня получилось

export const NewAnimatedButtonWithLinkUtm = ({   isDark = false,   btnColor = '#0A85D1', }: IAnimatedButtonProps) => {   const { ref, inView } = useInView();    return (     <div       ref={ref}       className={twMerge(         'sticky bottom-8 top-[80vh] mx-auto mt-8 flex h-[56px] cursor-pointer items-center justify-between gap-[16px] rounded-[10px] px-4 py-[8px] shadow-md backdrop-blur-[3.5px] transition-all',         isDark ? 'bg-[#424245]/70' : 'bg-[#D8D8D8]/30',         inView ? `opacity-1 w-[315px] delay-500 duration-1000` : 'w-[65px] opacity-0'       )}     >       <LinkWithUtm         href={linkHref}         className={twJoin(           'm-0 text-[17px] font-medium transition-all after:absolute after:inset-0',           isDark ? 'bg-[#424245]/70 text-[#F5F5F7]' : 'bg-transparent text-black',           inView ? 'opacity-1 delay-1000 duration-1000' : 'opacity-0'         )}       >         Начать учиться бесплатно       </LinkWithUtm>       <div         className={twJoin(           `absolute right-3 h-[40px] w-[40px] min-w-[40px] animate-pulse rounded-[10px] p-2 transition-all bg-[${btnColor}]`,           inView ? 'opacity-1' : 'opacity-0'         )}       >         <Image src={arrowRight} alt="" />       </div>     </div>   ); };

Как вы видите вместо 85 строк кода и сложной логикой, у меня 36 строк кода и стили применяется через логическое значение. Думаю будет намного проще рефакторить такой код 🙂

НО. На этом еще не все. Проблема была в том, что он появлялся сразу, как секция попадала во вьюпорт пользователя, а нам надо чтобы пользователь проскролил 30% секции и только после этого анимация срабатывала. И тут честно я немного затормозил и начал гуглить, хотя спустя пару часов логика оказалась очень простой.

Я создал контейнер в котором лежит кнопка и через children принимает секцию, по которой будет скролит пользователь и вот что получилось 🙂

export const SectionNewAnimatedButtonWithLinkUtm = ({   isDark = false,   btnClassName,   children,   className = 'relative mx-auto mb-40 max-w-[1024px] px-5 lg:px-0',   text = 'Начать учиться бесплатно', }: IAnimatedButtonProps) => {   const { ref, inView } = useInView({     threshold: 0.2,   });    return (     <section ref={ref} className={className}>       {children}       <div         className={twMerge(           'sticky bottom-8 top-[80vh] mx-auto mt-8 flex h-[56px] cursor-pointer items-center justify-between gap-[16px] rounded-[10px] px-4 py-[8px] shadow-md backdrop-blur-[3.5px] transition-all',           isDark ? 'bg-[#424245]/70' : 'bg-[#D8D8D8]/30',           inView             ? `opacity-1 w-[315px] delay-500 duration-1000`             : 'w-[65px] opacity-0 duration-1000'         )}       >         <LinkWithUtm           href={linkHref}           className={twJoin(             'm-0 text-[17px] font-medium transition-all after:absolute after:inset-0',             isDark ? 'bg-[#424245]/70 text-[#F5F5F7]' : 'bg-transparent text-black',             inView ? 'opacity-1 delay-1000 duration-200' : 'opacity-0'           )}         >           {text}         </LinkWithUtm>         <div           className={twMerge(             `absolute right-3 h-[40px] w-[40px] min-w-[40px] animate-pulse rounded-[10px] bg-[#0A85D1] p-2 transition-all`,             `${btnClassName}`,             inView ? 'opacity-1' : 'opacity-0'           )}         >           <Image src={arrowRight} alt="" />         </div>       </div>     </section>   ); };

Прибавилось 10 строк кода, но ничего страшного. Код все также остается понятным 🙂

Вроде бы все, можно открывать ПР и ждать ответа от Тимлида и мержить в основную ветку.

НОООО… Мы можем еще прокачать свой компонент и соблюдать один из принципов ООП — Open/Closed и спрятать всю логику под капот, чтобы фронтенд разработчик в команде пользовался компонентом и ему не приходилось залазить в кишки.

И тут решил я связать контейнер и компонент кнопки через обычный контекст в реакте 🙂
Впервую очередь мы создаем контекст и контейнер и передаем основное значение для анимации

export const AnimatedButtonContext = createContext<IAnimatedButtonContext>(null);  export const SectionAnimatedButton = ({ children }) => {   const { ref, inView } = useInView({     threshold: 0.2,   });    return (     <AnimatedButtonContext.Provider value={{ inView }}>       <section className="relative mx-auto mb-40 max-w-[1024px] px-5 lg:px-0" ref={ref}>         {children}         <AnimatedBtn />       </section>     </AnimatedButtonContext.Provider>   ); };

Затем в самой кнопке и используем хук useContext и привязываемся к контексту

export const AnimatedBtn = ({   isDark = false,   btnClassName,   text = 'Начать учиться бесплатно',   link = '', }: IAnimatedButtonProps) => {   const { inView } = useContext<IAnimatedButtonContext>(AnimatedButtonContext);    return (     <div       className={twMerge(         'sticky bottom-8 top-[80vh] mx-auto mt-8 flex h-[56px] cursor-pointer items-center justify-between gap-[16px] rounded-[10px] px-4 py-[8px] shadow-md backdrop-blur-[3.5px] transition-all',         isDark ? 'bg-[#424245]/70' : 'bg-[#D8D8D8]/30',         inView ? `opacity-1 w-[315px] delay-500 duration-1000` : 'w-[65px] opacity-0 duration-1000'       )}     >       <LinkWithUtm         href={link}         className={twJoin(           'm-0 text-[17px] font-medium transition-all after:absolute after:inset-0',           isDark ? 'bg-[#424245]/70 text-[#F5F5F7]' : 'bg-transparent text-black',           inView ? 'opacity-1 delay-1000 duration-200' : 'opacity-0'         )}       >         {text}       </LinkWithUtm>       <div         className={twMerge(           `absolute right-3 h-[40px] w-[40px] min-w-[40px] animate-pulse rounded-[10px] bg-[#0A85D1] p-2 transition-all`,           `${btnClassName}`,           inView ? 'opacity-1' : 'opacity-0'         )}       >         <Image src={arrowRight} alt="" />       </div>     </div>   ); };

Теперь компонент ГОТОВ 🙂

Что в итоге этой работы мы добились:
1 — Убрали тяжелую библиотеку
2 — Сделаю логику кнопки проще и следовательно рефакторить станет гораздо проще
3 — Спрятали логику и создали контейнер, в который мы заворачиваем наш контент и все работает 🙂
4 — Анимация стала работать гладко и без зависаний, в том числе и на слабых ПК

Статья получилась немного сумбурная, но этим хотел сказать, что применение библиотек не всегда хорошо и иногда полезно залезть в ту самую БАЗУ и посмотреть простое решение 🙂

Спасибо всем кто дочитал! Был рад рассказать про парт-тайм проект, на котором работаю и поэтому хочу сказать что я открыт для предложений к офферам и если вам нужен активный и амбициозный разработчик — буду рад с вами пообщаться и пофлексить вашим кодом 🙂
https://t.me/sadbatya

А с вами был Владимир! Хорошего дня, вечера и ночи 🙂


ссылка на оригинал статьи https://habr.com/ru/articles/874302/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *