dev-resources.site
for different kinds of informations.
How to improve the Frontend part of the project using one button as an example :))))
Hello everyone! I am a junior frontend developer. And I would like to tell you how sometimes using libraries in a project is an excess that should be avoided.
Let's start in order. The project is written in NextJs, TS, TailwindCSS. And there is an animated button on the site that appears beautifully when scrolling and opens a modal when clicked.
All the logic is written in Gsap. And it would seem that everything is fine, the library does the job for us, draws animations, and we calmly drink tea and send PR to GitHub.
BUT. The problem was that sometimes the animation froze, twitched, and on weak PCs it refused to work at all.
In the end, I decided to take on the animation button and see what's inside it :)
And, oh, MIRACLE! If you look at the component, it seems very scary to me. What do you think?
`export const AnimatedButtonLinkWitmUtm: React.FC = ({
isDark = false,
btnColor = '#0A85D1',
btnShadow = '0 0 0 0px rgb(157,52,218)',
maxWidth = 315,
}) => {
const buttonRef = useRef();
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 (
<>
ref={buttonRef}<br>
className={cn(<br>
'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]',<br>
{<br>
'bg-[#D8D8D8]/30': !isDark,<br>
'bg-[#424245]/70': isDark,<br>
}<br>
)}<br>
><br>
href="https://a6b9d8dc-b142-4b92-b1d0-dfbfd2230471.selstorage.ru/assets/edu-program.pdf"<br>
target="_blank"<br>
className={cn(<br>
'm-0 text-[17px] font-medium leading-[27.2px] opacity-0 after:absolute after:inset-0',<br>
{<br>
'text-black': !isDark,<br>
'text-[#F5F5F7]': isDark,<br>
}<br>
)}<br>
><br>
Подробнее о программе<br>
<br>
className="h-[40px] w-[40px] min-w-[40px] rounded-[10px] p-[8px] opacity-0"<br>
style={{ backgroundColor: btnColor }}<br>
><br>
<br>
<br>
<br>
</><br>
);
};
`
We have logic on GSAP and a trigger at what point of scrolling it should appear.
If you look at the animation, it is based on the size and opacity.
Do we need GSAP for such a simple animation??? NO. And let me try to explain why.
1 - Library weight. For the sake of the simplest animation, you pull a library weighing 4 MB into the project.
2 - This animation can be done in pure CSS
We will write the animation, but for the trigger, so that everything works, we only need Observer. All this can be implemented using hooks, but for such a situation there is a library much lighter and easier to use and under the hood it has React hooks.
As you can see, the package is much lighter and more popular in terms of downloads, as it covers simple needs for triggers and animations:)
And what did I end up with?
`export const NewAnimatedButtonWithLinkUtm = ({
isDark = false,
btnColor = '#0A85D1',
}: IAnimatedButtonProps) => {
const { ref, inView } = useInView();
return (
ref={ref}<br>
className={twMerge(<br>
'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',<br>
isDark ? 'bg-[#424245]/70' : 'bg-[#D8D8D8]/30',<br>
inView ? `opacity-1 w-[315px] delay-500 duration-1000` : 'w-[65px] opacity-0'<br>
)}<br>
><br>
href={linkHref}<br>
className={twJoin(<br>
'm-0 text-[17px] font-medium transition-all after:absolute after:inset-0',<br>
isDark ? 'bg-[#424245]/70 text-[#F5F5F7]' : 'bg-transparent text-black',<br>
inView ? 'opacity-1 delay-1000 duration-1000' : 'opacity-0'<br>
)}<br>
><br>
Начать учиться бесплатно<br>
<br>
className={twJoin(<br>
`absolute right-3 h-[40px] w-[40px] min-w-[40px] animate-pulse rounded-[10px] p-2 transition-all bg-[${btnColor}]`,<br>
inView ? 'opacity-1' : 'opacity-0'<br>
)}<br>
><br>
<br>
<br>
<br>
);
};`
As you can see, instead of 85 lines of code and complex logic, I have 36 lines of code and the styles are applied via a logical value. I think it will be much easier to refactor such code)
BUT. That's not all. The problem was that it appeared immediately as the section entered the user's viewport, and we need the user to scroll 30% of the section and only after that the animation would work. And here, honestly, I slowed down a little and started googling, although after a couple of hours the logic turned out to be very simple.
I created a container in which the button is located and through children it receives the section along which the user will scroll and this is what happened))
`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 (
{children}
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'
)}
>
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}
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'
)}
>
);
};`
10 lines of code have been added, but that's okay. The code is still clear)
It seems that's all, you can open the PR and wait for a response from the Team Lead and merge into the main branch.
NOOOO)))))) We can still pump up our component and follow one of the OOP principles - Open/Closed and hide all the logic under the hood so that the frontend developer in the team uses the component and does not have to get into the guts.
And then I decided to link the container and the button component through a regular context in React)
First of all, we create a context and a container and pass the main value for the animation
`export const AnimatedButtonContext = createContext(null);
export const SectionAnimatedButton = ({ children }) => {
const { ref, inView } = useInView({
threshold: 0.2,
});
return (
{children}
);
};`
Then in the button itself we use the useContext hook and bind to the context
`export const AnimatedBtn = ({
isDark = false,
btnClassName,
text = 'Начать учиться бесплатно',
link = '',
}: IAnimatedButtonProps) => {
const { inView } = useContext(AnimatedButtonContext);
return (
className={twMerge(<br>
'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',<br>
isDark ? 'bg-[#424245]/70' : 'bg-[#D8D8D8]/30',<br>
inView ? `opacity-1 w-[315px] delay-500 duration-1000` : 'w-[65px] opacity-0 duration-1000'<br>
)}<br>
><br>
href={link}<br>
className={twJoin(<br>
'm-0 text-[17px] font-medium transition-all after:absolute after:inset-0',<br>
isDark ? 'bg-[#424245]/70 text-[#F5F5F7]' : 'bg-transparent text-black',<br>
inView ? 'opacity-1 delay-1000 duration-200' : 'opacity-0'<br>
)}<br>
><br>
{text}<br>
<br>
className={twMerge(<br>
`absolute right-3 h-[40px] w-[40px] min-w-[40px] animate-pulse rounded-[10px] bg-[#0A85D1] p-2 transition-all`,<br>
`${btnClassName}`,<br>
inView ? 'opacity-1' : 'opacity-0'<br>
)}<br>
><br>
<br>
<br>
<br>
);
};`
Now the component is READY)
What we have achieved as a result of this work:
1 - Removed a heavy library
2 - I will make the button logic simpler and therefore it will be much easier to refactor
3 - Hid the logic and created a container in which we wrap our content and everything works))
4 - Animation began to work smoothly and without freezing, including on weak PCs
The article turned out to be a little chaotic, but I wanted to say that using libraries is not always good and sometimes it is useful to get into that very DATABASE and look at a simple solution)
Thanks to everyone who read it to the end! I was glad to tell you about the part-time project I work on and therefore I want to say that I am open to offers to offers and if you need an active and ambitious developer, I will be glad to talk to you and flex your code :)
https://t.me/sadbatya
And with you was Vladimir! Have a nice day, evening and night :)
Featured ones: