- Published on
Component Architecture
Designing and implementing the structure of your React components is an important aspect of web development that has a significant impact on code maintenance, scalability, and readability. The architecture of your components influences how your app behaves, so it's crucial to consider the various methods you can utilize. Here are several popular approaches, each with its unique advantages and potential drawbacks:
Below is more in-depth example of each method
Components Mapping Method:
The Components Mapping approach is a dynamic method where you employ an object to map different components. This approach is highly extensible and contributes to keeping your code clean and easy to read. Here's how it might look:The significant advantages of this method are its flexibility and ease of code extension. However, it can be a bit complex when you first encounter it.
const components = { component1: <Component1 />, component2: <Component2 />, // Add more as needed }; // Rendering component render() { return <div>{this.components[this.props.componentName]}</div>; }
Clsx Tailwind Variant:
If you're working with the Tailwind CSS framework, consider the Clsx library. It offers a mechanism for composing class names dynamically, which can be particularly beneficial when prototyping rapidly. Here's a basic example:Although this method is extremely powerful for fast prototyping, it can become verbose and potentially challenging to read as your application scales.
import clsx from 'clsx' function Component() { return <div className={clsx('text-center', 'text-blue-500')}>Hello, world!</div> }
SCSS className Composition:
If you favor working with SCSS, this approach provides excellent opportunities for extensibility, composition, and reusability. This method requires a solid understanding of SCSS, but its readability and structuring power can be a real boon. Here's an example of how this method can be employed:One downside is the additional setup needed for SCSS processing. However, the payoff in clean, reusable code can make this investment well worthwhile.
.btn { // common button styles &--primary { // primary button styles } &--secondary { // secondary button styles } }
import Agree from './components/Agree'
import Continue from './components/Continue'
interface ModalProps {
title: string;
body: string;
onClose: () => void;
onAccept: () => void;
onDecline?: () => void;
focusLock: boolean;
setModalStatus: React.Dispatch<React.SetStateAction<boolean>>;
modalRef: React.RefObject<HTMLDivElement>;
closeButtonRef: React.RefObject<HTMLButtonElement>;
buttonVariant: string | "outline" | "solid";
modalType: "Agree" | "Continue" | string;
theme?: any;
maskColor?: string;
backgroundMask?: boolean;
}
function ModalBody(props: ModalProps) {
const { modalType, ...otherProps } = props;
// Create a components mapping
const ComponentsMap = {
agree: Agree,
continue: Continue
};
// Dynamically get the component type
const ComponentToRender = ComponentsMap[modalType];
// Handle unknown modalType
if (!ComponentToRender) {
throw new Error(`Invalid prop modalType: ${modalType}`);
}
// Render the chosen component
return <ComponentToRender {...otherProps} />;
}
export default ModalBody
import * as React from 'react';
import { IconType } from 'react-icons'
import { ImSpinner2 } from 'react-icons/im'
import clsxm from '../../../../../lib/clsxm'
const ButtonVariant = ['primary', 'outline', 'ghost', 'light', 'dark'] as const;
const ButtonSize = ['sm', 'base'] as const;
type ButtonProps = {
isLoading?: boolean;
isDarkBg?: boolean;
variant?: (typeof ButtonVariant)[number];
size?: (typeof ButtonSize)[number];
leftIcon?: IconType;
rightIcon?: IconType;
leftIconClassName?: string;
rightIconClassName?: string;
} & React.ComponentPropsWithRef<'button'>;
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({
children,
className,
disabled: buttonDisabled,
isLoading,
variant = 'primary',
size = 'base',
isDarkBg = false,
leftIcon: LeftIcon,
rightIcon: RightIcon,
leftIconClassName,
rightIconClassName,
...rest
},ref) => {
const disabled = isLoading || buttonDisabled;
return (
<button
ref={ref}
type='button'
disabled={disabled}
className={clsxm(
'inline-flex items-center rounded font-medium',
'focus-visible:ring-primary-500 focus:outline-none focus-visible:ring',
'shadow-sm',
'transition-colors duration-75',
//#region //*=========== Size ===========
[
size === 'base' && ['px-3 py-1.5', 'text-sm md:text-base'],
size === 'sm' && ['px-2 py-1', 'text-xs md:text-sm'],
],
//#endregion //*======== Size ===========
//#region //*=========== Variants ===========
[
variant === 'primary' && [
'bg-primary-500 text-white',
'border-primary-600 border',
'hover:bg-primary-600 hover:text-white',
'active:bg-primary-700',
'disabled:bg-primary-700',
],
variant === 'outline' && [
'text-primary-500',
'border-primary-500 border',
'hover:bg-primary-50 active:bg-primary-100 disabled:bg-primary-100',
isDarkBg &&
'hover:bg-gray-900 active:bg-gray-800 disabled:bg-gray-800',
],
variant === 'ghost' && [
'text-primary-500',
'shadow-none',
'hover:bg-primary-50 active:bg-primary-100 disabled:bg-primary-100',
isDarkBg &&
'hover:bg-gray-900 active:bg-gray-800 disabled:bg-gray-800',
],
variant === 'light' && [
'bg-white text-gray-700',
'border border-gray-300',
'hover:text-dark hover:bg-gray-100',
'active:bg-white/80 disabled:bg-gray-200',
],
variant === 'dark' && [
'bg-gray-900 text-white',
'border border-gray-600',
'hover:bg-gray-800 active:bg-gray-700 disabled:bg-gray-700',
],
],
//#endregion //*======== Variants ===========
'disabled:cursor-not-allowed',
isLoading &&
'relative text-transparent transition-none hover:text-transparent disabled:cursor-wait',
className
)}
{...rest}
>
{isLoading && (
<div
className={clsxm(
'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2',
{
'text-white': ['primary', 'dark'].includes(variant),
'text-black': ['light'].includes(variant),
'text-primary-500': ['outline', 'ghost'].includes(variant),
}
)}
>
<ImSpinner2 className='animate-spin' />
</div>
)}
{LeftIcon && (
<div
className={clsxm([
size === 'base' && 'mr-1',
size === 'sm' && 'mr-1.5',
])}
>
<LeftIcon
className={clsxm(
[
size === 'base' && 'md:text-md text-md',
size === 'sm' && 'md:text-md text-sm',
],
leftIconClassName
)}
/>
</div>
)}
{children}
{RightIcon && (
<div
className={clsxm([
size === 'base' && 'ml-1',
size === 'sm' && 'ml-1.5',
])}
>
<RightIcon
className={clsxm(
[
size === 'base' && 'text-md md:text-md',
size === 'sm' && 'md:text-md text-sm',
],
rightIconClassName
)}
/>
</div>
)}
</button>
);
}
);
export default Button
import React, { useState, useRef, useEffect } from 'react';
import Icon from '../../IconWrapper/Icon';
import './button.scss';
import classNames from 'classnames';
import styles from './bubble.module.scss'
interface ButtonProps {
// available colors = {'red' ,'orange', 'blue'}
color?: string;
children?: React.ReactNode;
icon?: boolean;
id?: string;
size?: number;
shape?: string;
dot?: boolean;
dotColor?: string;
outlineColor?: string;
text?: string;
selected?: boolean;
outlineHover?: boolean;
customColor?: string;
custom?: boolean;
callBack?: () => void;
padding?: string;
width?: string;
}
function RippleButton({ color, children, shape, dot, dotColor, outlineColor, selected, text, callBack, custom, customColor, padding }: ButtonProps) {
const [ripple, setRipple] = useState(false);
const handleClick = (event) => {
setRipple(true);
const rippleEl = event.currentTarget.querySelector('.c-ripple__circle');
const { top, left } = event.currentTarget.getBoundingClientRect();
const { clientX, clientY } = event;
const x = clientX - left;
const y = clientY - top;
rippleEl.style.top = `${y}px`;
rippleEl.style.left = `${x}px`;
setTimeout(() => setRipple(false), 400);
// setSelect(!select);
};
const buttonClasses = classNames('c-button', {
[`c-button--${color}`]: true,
[`c-button--${shape}`]: shape,
[`c-button--outline-${outlineColor}`]: outlineColor,
'is-selected': selected,
'custom': shape === 'rounded' && custom
});
const dotClasses = classNames(styles[`bubble__${dotColor}-dot`]);
const rippleClasses = classNames('c-ripple', {
[`c-ripple--${shape}`]: shape,
'js-ripple': true,
'is-active': ripple,
});
const buttonStyle = {
padding: padding || undefined,
};
return (
<div contentEditable={false} style={buttonStyle} // Apply the buttonStyle object to the button element
className={buttonClasses} type="button" onClick={(e) => { handleClick(e); callBack ? callBack() : null }}
>
{dot && (
<>
<div className={dotClasses}
></div>
<span className={styles["bubble__title"]}>{text}</span>
</>
)}
<div className={rippleClasses} >
<span className="c-ripple__circle"></span>
<span className={styles["bubble__title"]}>{text}</span>
</div>
{children}
</div>
);
}
export default RippleButton
import React from 'react';
import { cva } from 'class-variance-authority';
import testImage from '../../assets/image-1.jpg';
interface ICardProps {
title: string;
content: string;
imageSrc?: string;
theme?: 'light' | 'dark';
size?: 'small' | 'medium' | 'large';
interaction?: 'hoverable' | 'static';
}
const cardStyles = cva(["border rounded-lg shadow flex"], {
variants: {
theme: {
light: ["bg-white", "text-gray-900", "border-gray-200"],
dark: ["bg-gray-800", "text-white", "border-gray-700"]
},
size: {
small: ["text-sm"],
medium: ["text-base"],
large: ["text-lg"]
},
contentPosition: {
left: ["flex-row"],
right: ["flex-row-reverse"],
top: ["justify-start"],
},
interaction: {
hoverable: ["transform transition-transform duration-500 hover:scale-105"],
static: [""]
}
},
defaultVariants: {
theme: "light",
size: "medium",
interaction: "static"
},
});
const Card: React.FC<ICardProps> = (props) => {
const { cardType } = props;
const ComponentsMap = {
imageCard: CardImage,
textCard: CardText,
rowImage: CardRowImage
};
const ComponentToRender = ComponentsMap[cardType];
if (!ComponentToRender) {
throw new Error(`Invalid prop for 'cardType': ${cardType}. Expected one of: ${Object.keys(ComponentsMap).join(', ')}`);
}
return <ComponentToRender {...props} />;
};
export default Card;
function CardRowImage({ imageSrc, title, content, theme, size, interaction, cardType }) {
return (
<a href="#" className="flex flex-col items-center bg-white border border-gray-200 rounded-lg shadow md:flex-row md:max-w-xl hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700">
<img className="object-cover w-full rounded-t-lg h-96 md:h-auto md:w-48 md:rounded-none md:rounded-l-lg" src={imageSrc} alt="" />
<div className="flex flex-col justify-between p-4 leading-normal">
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">{title}</h5>
<p className="mb-3 font-normal text-gray-700 dark:text-gray-400">{content}</p>
</div>
</a>
)
}
function CardText({ title, content, theme, size, interaction, cardType }) {
const cardClass = cardStyles({ theme, size, interaction, cardType });
return (
<div className={cardClass}>
<div className="p-5">
<a href="#">
<h5 className="mb-2 text-2xl font-bold tracking-tight">{title}</h5>
</a>
<p className="mb-3 font-normal">{content}</p>
<a href="#" className="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
Read more
<svg className="w-3.5 h-3.5 ml-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M1 5h12m0 0L9 1m4 4L9 9" />
</svg>
</a>
</div>
</div>
)
}
function CardImage({ imageSrc, title, theme, size, interaction, content, cardType }) {
const cardClass = cardStyles({ theme, size, interaction, cardType });
return (
<div class="max-w-sm bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700">
<a href="#">
{imageSrc && (
<img src={imageSrc} alt={title} className="rounded-t-lg" />
)}
</a>
<div className="p-5">
<a href="#">
<h5 className="mb-2 text-2xl font-bold tracking-tight">{title}</h5>
</a>
<p className="mb-3 font-normal">{content}</p>
<a href="#" className="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
Read more
<svg className="w-3.5 h-3.5 ml-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M1 5h12m0 0L9 1m4 4L9 9" />
</svg>
</a>
</div>
</div>
)
}