styled-components / styled-components

Visual primitives for the component age. Use the best bits of ES6 and CSS to style your apps without stress 💅
https://styled-components.com
MIT License
40.51k stars 2.5k forks source link

Feature Request: Pass on undefined props to final component #4338

Open aarondill opened 3 months ago

aarondill commented 3 months ago

Environment

> pnpx envinfo --system --binaries --npmPackages styled-components,babel-plugin-styled-components --markdown
## System:
 - OS: Linux 6.10 Arch Linux
 - CPU: (8) x64 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
 - Memory: 3.24 GB / 7.41 GB
 - Container: Yes
 - Shell: 5.2.26 - /usr/bin/bash
## Binaries:
 - Node: 22.5.1 - /usr/local/bin/node
 - npm: 10.8.2 - /usr/local/bin/npm
 - pnpm: 9.5.0 - /usr/local/bin/pnpm
 - bun: 1.1.10 - ~/.local/share/bun/bin/bun
## npmPackages:
 - styled-components: ^6.1.12 => 6.1.12

Reproduction

import styled from "styled-components";

// An empty component that just logs it's props
const Component = (props: { prop: unknown }) => {
    console.log(props);
    return null;
};
const StyledComponent = styled(Component)``;

export default function Page() {
    return <StyledComponent prop={undefined}></StyledComponent>;
}

Steps to reproduce

  1. Set up reactjs with Styled-Components.
  2. Run the above code
  3. Change StyledComponent in Page to Component

Expected Behavior

In both cases, Component should log { prop: undefined, className: "..." }

Actual Behavior

Component only receives the undefined prop when not wrapped in styled.

Real World Implications

This is an issue when using @radix-ui/react-dialog, since the Dialog.Content expects you to pass aria-describedby={undefined} if there's no Dialog.Description, which is impossible without an ugly wrapper.

const _DialogContent = forwardRef<
    HTMLDivElement,
    Dialog.DialogContentProps & { hasDescription: boolean }
>((props, ref) => {
    const { hasDescription, ...rest } = props;
    const p = hasDescription ? {} : { "aria-describedby": undefined };
    return <Dialog.Content {...p} {...rest} ref={ref} />;
});
const StyledContent = styled(_DialogContent)``
// ...
// usage:
<StyledContent hasDescription={!!description}>
...
</StyledContent>
aarondill commented 3 months ago

It's worth noting that this was an intentional decision (see the code in src/models/StyledComponent.ts), however, allowing undefined to be passed would have the same effect as not passing it, except when using "key" in obj. https://github.com/styled-components/styled-components/blob/e0019ba666fab4b5aaa2bff71ba6ad0005a299fd/packages/styled-components/src/models/StyledComponent.ts#L142-L146

aarondill commented 3 months ago

Since the props passed to the final outermost styled component (StyledComponent in the example) and .attrs() follow this same code path, this issue affects them both.

mtsknn commented 1 month ago

This worked in v5, or at least I encountered this problem after migrating from v5 to v6. This breaking change isn't mentioned in the v5 to v6 migration guide.

My use case: I have a Link component (<a>) which defaults to rel="noopener noreferrer"; setting rel={undefined} doesn't work anymore.

Lukas-Kullmann commented 3 weeks ago

For what it's worth, this also causes this bug in Material UI when using styled-components: https://github.com/mui/material-ui/issues/44185

The problem is that you now cannot pass on a prop as explicitly undefined to reset defaults further down. In case of Material UI, it looks something like this:

const ButtonBase = (props) => {
   // ...
  // `role="button"` will not be reset here since styled-components strips away the `role={undefined}`
   return <ButtonBaseRoot role="button" {...props} />;
}

const SwitchBaseRoot = styled(ButtonBase)``;

const SwitchBase = () => {
  // ...
  return <SwitchBaseRoot role={undefined} />;
}