DEV Community

Cover image for Using Emotion in a New Project: Component Inheritance vs. Single Styled Component with Props
Maria Kim
Maria Kim

Posted on

Using Emotion in a New Project: Component Inheritance vs. Single Styled Component with Props

I was creating shared (common) components using Emotion in a new project. As I was considering how best to structure things for cleanliness and scalability, I happened to watch a YouTube video about the SOLD principle. That video led me to think that developing shared components using style inheritance could be advantageous for future expansion.

So I changed all my shared components to use extension (inheritance). However, I encountered an unexpected bug in the process. Through this bug, I learned the difference between:

  1. Extending (inheriting) components

  2. Using branching within a single styled component

And I'd like to share that experience here.

Two Ways to Handle Variations in Components

1. Using Inheritance (Extend)

  1. Create a base styled component.
  2. For each needed variant, extend that base component and apply different styles.
  3. If you have many variants, adding new ones is straightforward since it simply involves creating more extended components.

2. Handling Branching within a Single Styled Component

  1. Pass a value like variant as a prop.
  2. In a single styled component, use conditionals such as switch or ternary operators to apply different styles.
  3. This approach handles frequently changing props dynamically and immediately.

Simple Example Code Comparison

1. Handling Variants with Inheritance

function Button({ variant = "primary", disabled, children }: ButtonProps) {
  const Component = ButtonComponent[variant];
  return <Component disabled={disabled}>{children}</Component>;
}

const StyledButtonBase = styled.button`
  /* Common styles */
`;

const PrimaryButton = styled(StyledButtonBase)`
  background-color: black;
  color: white;
`;

const DangerButton = styled(StyledButtonBase)`
  background-color: red;
  color: white;
`;

const ButtonComponent = {
  primary: PrimaryButton,
  danger: DangerButton
};
Enter fullscreen mode Exit fullscreen mode

• When a new variant is added, you extend StyledButtonBase again (for example, DangerButton) and can easily keep expanding.

2. Handling Branching within a Single Styled Component

function Button({ variant = "primary", disabled, children }: ButtonProps) {
  return (
    <StyledButton variant={variant} disabled={disabled}>
      {children}
    </StyledButton>
  );
}

const StyledButton = styled.button<ButtonProps>`
  /* Common styles */
  background-color: ${(props) => getBackgroundColor(props)};
  color: ${(props) => getColor(props)};
`;

function getBackgroundColor({ variant, disabled, theme }: ButtonProps & { theme: Theme }) {
  switch (variant) {
    case "primary":
      return blue;
    case "danger":
      return red;
    default:
      return gray;
  }
}

function getColor({ variant, disabled, theme }: ButtonProps & { theme: Theme }) {
  switch (variant) {
    case "primary":
      return white;
    case "danger":
      return white;
    default:
      return black;
  }
}
Enter fullscreen mode Exit fullscreen mode

*This is not a good example. The button color may change depending on the state after the button component is mounted. Instead of this approach, it is better to use Styled Component inheritance when there are design variations, such as a Background button and a Border button.

The Problem Scenario

Let's say we have an InputContainer component that wraps an input. When the input's value changes (onChange), we perform a validation check. If there's an error, we pass variant='error' to change the border color to red.

Inheritance-Based InputContainer

function InputContainer({ variant = "default", children }: InputContainerProps) {
  const Component = InputContainerComponent[variant];
  return <Component>{children}</Component>;
}

const BaseInputContainer = styled.div`
  border: 1px solid black;
`;

const ErrorInputContainer = styled(BaseInputContainer)`
  border-color: red;
`;

const InputContainerComponent = {
  default: BaseInputContainer,
  error: ErrorInputContainer,
};
Enter fullscreen mode Exit fullscreen mode

Consider that a re-render takes place. What potential issue could arise here?

Handling Branching within a Single Styled Component InputContainer

function InputContainer({ variant = "default", children }: InputContainerProps) {
  return (
    <StyledInputContainer variant={variant}>
      {children}
    </StyledInputContainer>
  );
}

const StyledInputContainer = styled.div<{ variant: "default" | "error" }>`
  border: 1px solid
    ${({ theme, variant }) =>
      variant === "error" ? theme.colors.variants.negative : theme.colors.neutral.gray300};
  /* ... */
`;
Enter fullscreen mode Exit fullscreen mode

What differences do you see in how the component is re-rendered?

The Bug I Encountered

I experienced a bug where the input's focus would disappear every time the border color changed.

Why Did This Happen?

In the inheritance-based approach, changing the variant creates what is effectively a new component each time. So when variant changes from "default" to "error", React sees the old component unmount and a new one mount. As a result, the original input element is removed, and a new one is created, causing the loss of focus.

What about the branch inside style approach?

With props, you're still dealing with the same component. Only the class or inline styles change. Because the same DOM node persists, the focus doesn't get lost when the border color changes.

Summary

Inheritance-Based InputContainer

• When the variant changes, a different component is rendered.
• React may treat it as a completely separate DOM element and re-mount it, depending on the complexity of the structure.
• If you had an input focused inside, it can lose focus due to re-mount.

Branch inside style InputContainer

• When the variant changes, the same component remains; only the styling changes.
The focused input remains intact, preserving the focus.

Conclusion & Recommendations

When Should You Use Inheritance?

• The prop that determines branching will never change after the component mounts.
• For example, a Button component with multiple design variations (e.g., Border, Background, etc.).
• Inheritance aligns well with the Open-Closed Principle, allowing new variants to be added without modifying existing code.

When Should You Use Props-Based Branching?

When there's a lot of user interaction and the variant or disabled states change frequently.
• For example, form inputs that can switch to an error state in real-time.
• Because the component doesn't unmount and remount, states like focus are maintained.

In the case of Emotion, you can define only the class separately, allowing for extension in accordance with the Open-Closed Principle

By using Emotion’s css utility as shown below, you can apply the Open-Closed Principle by changing only the class.

import { css } from "@emotion/react";
import styled from "@emotion/styled";

import theme from "@/styles/theme";

export type InputVariant = "default" | "error";

interface InputContainerProps {
  children: React.ReactNode;
  className?: string;
  variant?: InputVariant;
}

function InputContainer({
  children,
  className,
  variant = "default",
}: InputContainerProps) {
  return (
    <StyledInputContainer variant={variant} className={className}>
      {children}
    </StyledInputContainer>
  );
}

export default InputContainer;

interface StyledInputContainerProps {
  variant: InputVariant;
}

const StyledInputContainer = styled.div<StyledInputContainerProps>`
  display: flex;
  padding: 11px 18px;
  justify-content: space-between;
  border-radius: 5px;
  background: ${({ theme }) => theme.colors.neutral.white};
  ${({ variant }) =>
    CustomInputContainer[variant] ?? CustomInputContainer.default};
`;

const defaultInputContainer = css`
  border: 1px solid ${theme.colors.neutral.gray100};
`;

const errorInputContainer = css`
  border: 1px solid ${theme.colors.variants.negative};
`;

const CustomInputContainer: Record<
  NonNullable<InputContainerProps["variant"]>,
  typeof defaultInputContainer
> = {
  default: defaultInputContainer,
  error: errorInputContainer,
};
Enter fullscreen mode Exit fullscreen mode

Final Summary

  • For components with frequent real-time UI changes: Handle branching within a single component.

  • For components that do not change dynamically after mounting and need to be extended with multiple types: Use inheritance.

  • If necessary, you can mix both approaches. For example, for a Button component, you can distinguish broad categories (such as design differences like Border and Background) using inheritance, while handling more detailed state changes (such as error or success) via props.

Ultimately, it's crucial to understand "when the UI changes". I hope this post helps you design shared components with Emotion more effectively!

Thank you!

Top comments (0)