I have this Button.jsx
file which exports a <Button />
component, that wraps a <StyledButton />
component.
(I need this wrap to handle extra props like loading
and spinner
)
The <StyledButton />
component checks for a $hasIcon
prop to fix the width
based on an existing icon in the button:
// Button.jsx
import styled from 'styled-components';
const StyledButton = styled.button`
width: ${ ({ $hasIcon }) => $hasIcon ? 'fit-content' : '100%' };
background-color: red;
`;
export const Button = ({ loading, spinner, children, ...props }) => (
<StyledButton { ...props }>
{ children }
{ loading ? spinner : null }
</StyledButton>
);
Then, I use the <Button />
component from Button.jsx
in my whole project without problems, but things start getting weird when I extend its styles to overwrite some of them.
// SomeComponent.jsx
import styled from 'styled-components';
import Button from '@components/Button';
import { SomeIcon } from '@icons';
const ExtendedStyleButton = styled(Button)`
background-color: green;
`;
export const SomeComponent = () => (
<ExtendedStyleButton $hasIcon>
<SomeIcon />
</ExtendedStyleButton>
);
$hasIcon
prop passed to <ExtendedStyleButton />
should reach <StyledButton />
through the extended style of <Button />
in order to fix the width.
Desired path:
$hasIcon
|
▼
<ExtendedStyleButton />
| $hasIcon
▼
<Button />
| $hasIcon
▼
<StyledButton /> // fixed width to fit-content due to $hasIcon prop correctly passed
I have been debugging everything with console.log
, and I believe the $hasIcon
prop goes up to <ExtendedStyleButton />
and nothing else, which causes the width
not being fixed for an icon and taking a 100%
value.
Actual path:
$hasIcon
|
▼
<ExtendedStyleButton />
|
▼
<Button />
|
▼
<StyledButton /> // fixed width to 100% because $hasIcon prop has not been received
The $
(dollar) prefix character on props is interpreted by styled-components as a sign for transient props:
If you want to prevent props meant to be consumed by styled components from being passed to the underlying React node or rendered to the DOM element, you can prefix the prop name with a dollar sign (
$
), turning it into a transient prop.
That is why your <ExtendedStyleButton>
does not forward your $hasIcon
prop to its underlying styled <Button>
.
With version 5 of styled-components, if you can change your API, the simplest solution would be to rename your $hasIcon
prop to remove the $
prefix, e.g. into just hasIcon
. That way, styled-components no longer treats it as a transient prop, and happily forwards it to the underlying Component. Of course, you can still use it to do dynamic styling:
const StyledButton = styled.button`
width: ${ ({ hasIcon }) => hasIcon ? 'fit-content' : '100%' };
background-color: red;
`;
export const Button = /* unchanged */
const ExtendedStyleButton = /* unchanged */
export const SomeComponent = () => (
<ExtendedStyleButton hasIcon>
<SomeIcon />
</ExtendedStyleButton>
);
Styled-components still makes sure not to forward it to the base <button>
however, because it knows <button>
is a raw DOM Element (not a React Custom Component), and it is not accepting attributes named "hasIcon":
The
styled
function is smart enough to filter non-standard attributes automatically for you.
Unfortunately, from version 6 of styled-components, this "smart enough" feature is no longer built-in:
If haven't migrated your styling to use transient props (
$prefix
), you might notice React warnings about styling props getting through to the DOM in v6. To restore the v5 behavior, useStyleSheetManager
import isPropValid from '@emotion/is-prop-valid';
import { StyleSheetManager } from 'styled-components';
function MyApp() {
return (
<StyleSheetManager shouldForwardProp={shouldForwardProp}>
{/* Rest of your App content */}
</StyleSheetManager>
)
}
// This implements the default behavior from styled-components v5
function shouldForwardProp(propName, target) {
if (typeof target === "string") {
// For HTML elements, forward the prop if it is a valid HTML attribute
return isPropValid(propName);
}
// For other elements, forward all props
return true;
}
Live demo: https://stackblitz.com/edit/vitejs-vite-d3xmzx
Of course, you still need to make sure to use custom prop names that do not happen to be standard attribute names (like fill
, src
, etc.), otherwise styled-components would stillpass them to the DOM Elements. An easy bullet-proof solution to avoid such collisions is to use a prefix, e.g. styleHasIcon
, or even just an underscore _hasIcon