How to implement S.O.L.I.D Principles in React and Easy Demo.

·

4 min read

Single Responsibility Principle (SRP).

Content: A component should have only one reason to change, meaning it should have only one job.

Should: Try splitting code into smaller component to easy to control and debug.

Shouldn't: Combine display logic, api fetching and business logic into one component.

// Should
const ResumeBuilderShould = ({ data }) => {
    return (
        <div>
            <Info data={data.info}/>
            <Project data={data.project}/> 
            <Education data={data.education} />
        </div>
    )
}

const Info = ({ data }) => (
    <div>
        <div>Name: {data.name}</div>
        <div>Address: {data.address}</div>
    </div>
)

const Project = ({ data }) => (
    <div>Project Info....</div>
)

const Education = () => (
    <div>Education Info ...</div>
)
// Shouldn't
const ResumeBuilderShouldNot = ({ data }) => {
    return (
        <div>
            <div>Name: {data.info.name}</div>
            <div>Address: {data.info.address}</div>
            <div>Project Info....</div>
            <div>Education Info....</div>
        </div>
    )
}

Open/Closed Principle (OCP)

Content: Software entities should be open for extension, but closed for modification.

Should: Use props to extend component functionalities without modifying the original component.

Shouldn't: Modify the original component code to add style or new behaviors directly.

// Should
const Input = ({ placeholder, onChange, className, ...props }) => {
    return (
        <input 
            {...props}
            onChange={onChange} 
            placeholder={placeholder} 
            className={className} 
        />
    )
}

// Usage with Custom Hook or HOC
const withActiveStyle = (Component) => ({ isActive, ...props }) => {
    const activeClass = isActive ? 'color-active' : 'color-primary';
    return <Component {...props} className={`${props.className} ${activeClass}`} />;
};

const CustomInput = withActiveStyle(Input);

// Shouldn't
const InputShouldNot = ({ placeholder, onChange, className, ...props }) => {
    const isActiveStyle = props.isActive ? 'color-active' : 'color-primary';
    return (
        <input 
            {...props}
            onChange={onChange} 
            placeholder={placeholder} 
            className={`${className} ${isActiveStyle}`} 
        />
    );
};

Liskov Substitution Principle (LSP)

Content: Objects of a superclass shall be replaceable with objects of its subclasses without breaking the application.

Should: Allow subclass component can replace superclass components.

Shouldn't: Write subclass-specific properties that break functionality when substituting.

// Should
const Label = ({ htmlFor, children, className }) => {
    return <label htmlFor={htmlFor} className={className}>{children}</label>
}

// Usage Label with Icon
const LabelIcon = ({ htmlFor, children, className, icon, iconClassName }) => {
    return (
        <label htmlFor={htmlFor} className={className}>
            {icon && <img src={icon} alt="icon" className={iconClassName} />}
            {children}
        </label>
    );
};

//Shouldn't
const LabelIconShouldNot = ({ htmlFor, className, icon, iconClassName }) => {
    if (!icon) return
    return (
        <label htmlFor={htmlFor} className={className}>
            <img src={icon} alt="icon" className={iconClassName} />
        </label>
    )
}

Interface Segregation Principle (ISP)

Content: No client should be forced to depend on methods it does not use

Should: Provide specific interfaces for different uses.

Shouldn't: Clutter a component with unnecessary properties.

// Should
// Base Button Component
const Button = ({ type, children, onClick, className }) => {
    let buttonClass = "btn";
    if (type === 'success') {
        buttonClass += " btn-success";
    } else if (type === 'primary') {
        buttonClass += " btn-primary";
    }

    return (
        <button type="button" className={`${buttonClass} ${className}`} onClick={onClick}>
            {children}
        </button>
    );
};

// Disabled Button from base Button Component
const DisabledButton = ({ children, className }) => {
    return <div className={`btn-disabled ${className}`}>{children}</div>;
};


// Shouldn't
const ButtonShouldNot = ({ type, children, onClick, isDisable }) => {
    if (isDisable && !onClick) {
        return <div className="btn-disable">{children}</div>;
    }

    return <button type={type} onClick={onClick}>{children}</button>;
};

Dependency Inversion Principle (DIP)

Content: High-level modules should not depend on low-level modules. Both should depend on abstractions

Should: Use hook, context, or similar patterns to abstract data fetching or state management.

Shouldn't: Hard-code data fetching inside component.

// Custom Hook for Layout
const useLayout = () => {
    const [layout, setLayout] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        const handleGetLayout = async () => {
            try {
                // Simulating data fetching
                const response = await fetch('/api/layout'); // replace with actual data fetching
                const data = await response.json();
                setLayout(data);
            } catch (err) {
                setError(err);
            } finally {
                setLoading(false);
            }
        };

        handleGetLayout();
    }, []);

    return { layout, loading, error };
};

// Layout Component
const Layout = () => {
    const { layout, loading, error } = useLayout();

    if (loading) return <div>Loading...</div>;
    if (error) return <div>Error: {error.message}</div>;

    return <div>{layout.name}</div>;
};

// Shouldn't
const LayoutShouldNot = () => {
    const [layout, setLayout] = useState(null);

    useEffect(() => {
        const handleGetLayout = async () => {
            // data fetching....
        };
        const updateLayout = handleGetLayout();
        setLayout(updateLayout);
    }, []);

    if (!layout) return <div>Loading...</div>;
    return <div>{layout.name}</div>;
};