styled-components, one more time

stereobooster - Jun 7 '19 - - Dev Community

UPD: About div vs button.

Originally styled-components as a library appeared in 2016. The idea was proposed by Glen Maddern. It is CSS-in-JS solution, but in my opinion, the most powerful part is not CSS-in-JS, but the ability to create small components fast.

Consider following snippet:

<h1 style={{ fontSize: "1.5em", textAlign: "center", color: "palevioletred" }}>
  Hello World, this is my first styled component!
</h1>

vs

const Title = styled.h1`
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`;

<Title>Hello World, this is my first styled component!</Title>;

As you can see there is more semantics in this code. You can easily tell that it is a title, it can be hard to tell otherwise (in case of h1 it is possible to guess, but if it would be div?..).

We can for sure do the same without styled-components:

const Title = ({ children }) => (
  <h1
    style={{ fontSize: "1.5em", textAlign: "center", color: "palevioletred" }}
  >
    {children}
  </h1>
);

But let's face it: nobody does it. Where is with styled-components it happens naturally. This is simplified version, it doesn't pass props, it doesn't handle ref property.

styled-components pair nicely with a11y. For example, instead of this:

<>
  <div
    role="button"
    aria-expanded={expanded}
    aria-controls={sectionId}
    id={labelId}
    className={styles.Label}
    onClick={() => onToggle && onToggle(index)}
  >
    {title}
    <span aria-hidden={true}>{expanded ? "" : ""}</span>
  </div>
  <div
    role="region"
    aria-labelledby={labelId}
    id={sectionId}
    hidden={!expanded}
    className={styles.Panel}
  >
    {expanded && (isFunction(children) ? children() : children)}
  </div>
</>

we can write

const Label = styled.div("Label");
Label.defaultProps = { role: "button" };

const Panel = styled.div("Panel");
Panel.defaultProps = { role: "region" };

<>
  <Label
    aria-expanded={expanded}
    aria-controls={sectionId}
    id={labelId}
    onClick={() => onToggle && onToggle(index)}
  >
    {title}
    <span aria-hidden={true}>{expanded ? "" : ""}</span>
  </Label>
  <Panel aria-labelledby={labelId} id={sectionId} hidden={!expanded}>
    {expanded && (isFunction(children) ? children() : children)}
  </Panel>
</>;

Nicer isn't it?

Alternatives

This idea is so popular that it got copied by other libraries.

CSS-in-JS

"Zero-runtime" CSS-in-JS

style property

CSS modules

DIY

Let's write (very) simplified implementation of styled-components, to demystify things a bit. So we started with this:

const Title = ({ children }) => (
  <h1
    style={{ fontSize: "1.5em", textAlign: "center", color: "palevioletred" }}
  >
    {children}
  </h1>
);

1) we need to pass props:

const Title = ({ children, ...props }) => (
  <h1
    style={{ fontSize: "1.5em", textAlign: "center", color: "palevioletred" }}
    {...props}
  >
    {children}
  </h1>
);

2) We need to handle ref

const Title = React.forwardRef(({ children, ...props }, ref) => (
  <h1
    style={{ fontSize: "1.5em", textAlign: "center", color: "palevioletred" }}
    ref={ref}
    {...props}
  >
    {children}
  </h1>
));

3) Let's make tag configurable with as property (some libraries call it is or use as well):

const Title = React.forwardRef(({ children, as = "h1", ...props }, ref) =>
  React.createElement(
    as,
    {
      style: { fontSize: "1.5em", textAlign: "center", color: "palevioletred" },
      ...props,
      ref
    },
    children
  )
);

4) Let's add a way to override styles

const Title = React.forwardRef(({ children, as = "h1", ...props }, ref) =>
  React.createElement(
    as,
    {
      ...props,
      style: {
        fontSize: "1.5em",
        textAlign: "center",
        color: "palevioletred",
        ...props.style
      },
      ref
    },
    children
  )
);

5) Let's generate component

const styled = defaultAs =>
  React.forwardRef(({ children, as = defaultAs, ...props }, ref) =>
    React.createElement(
      as,
      {
        ...props,
        style: {
          fontSize: "1.5em",
          textAlign: "center",
          color: "palevioletred",
          ...props.style
        },
        ref
      },
      children
    )
  );

const Title = styled("h1");

6) Let's pass default styles from outside

const styled = defaultAs => defaultStyles =>
  React.forwardRef(({ children, as = defaultAs, ...props }, ref) =>
    React.createElement(
      as,
      {
        ...props,
        style: {
          ...defaultStyles,
          ...props.style
        },
        ref
      },
      children
    )
  );

const Title = styled("h1")({
  fontSize: "1.5em",
  textAlign: "center",
  color: "palevioletred"
});

7) Add display name to the component

const styled = defaultAs => defaultStyles => {
  const component = React.forwardRef(
    ({ children, as = defaultAs, ...props }, ref) =>
      React.createElement(
        as,
        {
          ...props,
          style: {
            ...defaultStyles,
            ...props.style
          },
          ref
        },
        children
      )
  );
  component.displayName = `${defaultAs}💅`;
  return component;
};

8) Make styles customizable depending on properties

const isFunction = x => !!(x && x.constructor && x.call && x.apply);

const styled = defaultAs => defaultStyles => {
  const component = React.forwardRef(
    ({ children, as = defaultAs, ...props }, ref) =>
      React.createElement(
        as,
        {
          ...props,
          style: {
            ...(isFunction(defaultStyles)
              ? defaultStyles(props)
              : defaultStyles),
            ...props.style
          },
          ref
        },
        children
      )
  );
  component.displayName = `${defaultAs}💅`;
  return component;
};

const Title = styled("h1")(() => ({
  fontSize: "1.5em",
  textAlign: "center",
  color: "palevioletred"
}));

9) Filter non-html properties, let's filter out all properties in propTypes:

const filterObject = (rest, shouldForwardProp) =>
  Object.keys(rest)
    .filter(shouldForwardProp)
    .reduce((obj, key) => {
      obj[key] = rest[key];
      return obj;
    }, {});

const styled = defaultAs => defaultStyles => {
  const component = React.forwardRef(
    ({ children, as = defaultAs, ...props }, ref) =>
      React.createElement(
        as,
        {
          ...(component.propTypes
            ? filterObject(props, key =>
                Object.keys(component.propTypes).includes(key)
              )
            : props),
          style: {
            ...(isFunction(defaultStyles)
              ? defaultStyles(props)
              : defaultStyles),
            ...props.style
          },
          ref
        },
        children
      )
  );
  component.displayName = `${defaultAs}💅`;
  return component;
};

10) Bonus. Let's use Proxy instead of the first function:

const styled = Proxy(
  {},
  {
    get: (_, defaultAs, __) => defaultStyles => {
      const component = React.forwardRef(
        ({ children, as = defaultAs, ...props }, ref) =>
          React.createElement(
            as,
            {
              ...(component.propTypes
                ? filterObject(props, key =>
                    Object.keys(component.propTypes).includes(key)
                  )
                : props),
              style: {
                ...(isFunction(defaultStyles)
                  ? defaultStyles(props)
                  : defaultStyles),
                ...props.style
              },
              ref
            },
            children
          )
      );
      component.displayName = `${defaultAs}💅`;
      return component;
    }
  }
);

const Title = styled.h1({
  fontSize: "1.5em",
  textAlign: "center",
  color: "palevioletred"
});

Now you know what is inside!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .