基于Styled Components实现动画Button变体组件的复用与调用优化
Got it, let's fix that code duplication issue while keeping your desired API (<Button animatedButton text="test"/>) intact. Here's a step-by-step approach to reuse your base Button component for the animated variant:
1. Refactor the Base Button to Accept Variant Props
Instead of creating a separate AnimatedButton component, extend your existing Button to conditionally apply animated styles and markup based on a prop (like animatedButton). This way you reuse 100% of the base component's structure.
Example base component (updated):
import React, { useRef } from 'react'; import './Button.css'; const Button = ({ text, animatedButton = false, onClick, ...rest }) => { // Combine base class with animated variant class if needed const buttonClasses = `button ${animatedButton ? 'button--animated' : ''}`; const rippleRef = useRef(null); // Handle click + optional animation logic const handleClick = (e) => { // Execute the passed onClick handler first if (onClick) onClick(e); // Trigger ripple animation only if animatedButton is true if (animatedButton && rippleRef.current) { // Reset ripple state rippleRef.current.classList.remove('button__ripple--active'); // Calculate click position relative to the button const buttonRect = e.currentTarget.getBoundingClientRect(); const clickX = e.clientX - buttonRect.left; const clickY = e.clientY - buttonRect.top; // Position the ripple rippleRef.current.style.left = `${clickX}px`; rippleRef.current.style.top = `${clickY}px`; // Trigger animation with a tiny delay to ensure reset takes effect setTimeout(() => { rippleRef.current.classList.add('button__ripple--active'); }, 10); } }; return ( <button className={buttonClasses} onClick={handleClick} {...rest}> {/* Reuse the base text span for both variants */} <span className="button__text">{text}</span> {/* Add ripple element only for animated variant */} {animatedButton && <span ref={rippleRef} className="button__ripple"></span>} </button> ); }; export default Button;
2. Write CSS That Reuses Base Styles + Extends Animated Styles
Avoid duplicating base button styles by nesting or extending them for the animated variant. Here's how to structure your CSS:
/* Base Button Styles (reused by both variants) */ .button { padding: 0.8rem 1.5rem; border: none; border-radius: 4px; font-size: 1rem; cursor: pointer; background-color: #007bff; color: white; position: relative; overflow: hidden; transition: background-color 0.2s ease; } .button:hover { background-color: #0056b3; } .button__text { position: relative; z-index: 1; /* Ensures text stays above ripple */ } /* Animated Variant Extensions */ .button--animated { transition: transform 0.2s ease, background-color 0.2s ease; } .button--animated:active { transform: scale(0.95); /* Quick press animation */ } /* Ripple Animation Styles */ .button__ripple { position: absolute; top: 0; left: 0; width: 0; height: 0; border-radius: 50%; background-color: rgba(255, 255, 255, 0.3); transform: translate(-50%, -50%); transition: width 0.6s ease, height 0.6s ease; } .button__ripple--active { width: 300px; height: 300px; }
3. Use the Component As Requested
Now you can call both variants using the same Button component, just toggle the animatedButton prop:
// Basic Button <Button text="Basic Action" onClick={() => console.log('Basic click!')} /> // Animated Button (matches your desired API) <Button animatedButton text="Animated Action" onClick={() => console.log('Animated click!')} />
Bonus: Clean Up Complex Logic with Custom Hooks
If your animation logic gets more complex, extract it into a custom hook to keep the Button component clean:
// useButtonAnimation.js import { useRef } from 'react'; export const useButtonAnimation = (isAnimated) => { const rippleRef = useRef(null); const handleAnimation = (e) => { if (!isAnimated || !rippleRef.current) return; rippleRef.current.classList.remove('button__ripple--active'); const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; rippleRef.current.style.left = `${x}px`; rippleRef.current.style.top = `${y}px`; setTimeout(() => { rippleRef.current.classList.add('button__ripple--active'); }, 10); }; return { rippleRef, handleAnimation }; };
Then use it in your Button component:
const Button = ({ text, animatedButton = false, onClick }) => { const { rippleRef, handleAnimation } = useButtonAnimation(animatedButton); const buttonClasses = `button ${animatedButton ? 'button--animated' : ''}`; const handleClick = (e) => { onClick?.(e); handleAnimation(e); }; // Rest of the component markup remains the same };
This approach keeps your code DRY, reuses all base Button markup/styles, and fits exactly the API you wanted.
内容的提问来源于stack exchange,提问作者Kiwimoisi

