How to extend styled component without passing props to underlying DOM element?
Asked Answered
S

5

64

I have a styled component that is extending a third-party component:

import Resizable from 're-resizable';
...
const ResizableSC = styled(Resizable)``;
export const StyledPaneContainer = ResizableSC.extend`
    flex: 0 0 ${(props) => props.someProp}px;
`;


const PaneContainer = ({ children, someProp }) => (
    <StyledPaneContainer
        someProp={someProp}
    >
        {children}
    </StyledPaneContainer>
);

export default PaneContainer;

This resulted in the following error in the browser console:

Warning: React does not recognize the someProp prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase someProp instead. If you accidentally passed it from a parent component, remove it from the DOM element

So, I changed the prop to have a data-* attribute name:

import Resizable from 're-resizable';
...
const ResizableSC = styled(Resizable)``;
export const StyledPaneContainer = ResizableSC.extend`
    flex: 0 0 ${(props) => props['data-s']}px;
`;


const PaneContainer = ({ children, someProp }) => (
    <StyledPaneContainer
        data-s={someProp}
    >
        {children}
    </StyledPaneContainer>
);

export default PaneContainer;

This works, but I was wondering if there was a native way to use props in the styled component without them being passed down to the DOM element (?)

Soapstone answered 14/4, 2018 at 17:31 Comment(0)
S
93

2020 UPDATE: Use transient props

With the release 5.1.0 you can use transient props. This way you do not need an extra wrapper i.e. unnecessary code is reduced:

Transient props are a new pattern to pass props that are explicitly consumed only by styled components and are not meant to be passed down to deeper component layers. Here's how you use them:

const Comp = styled.div`
  color: ${props => props.$fg || 'black'};
`;

render(<Comp $fg="red">I'm red!</Comp>);

Note the dollar sign ($) prefix on the prop; this marks it as transient and styled-components knows not to add it to the rendered DOM element or pass it further down the component hierarchy.

The new answer should be:

styled component:

const ResizableSC = styled(Resizable)``;
export const StyledPaneContainer = ResizableSC.extend`
    flex: 0 0 ${(props) => props.$someProp}px;
`;

parent component:

const PaneContainer = ({ children, someProp }) => (
    <StyledPaneContainer
        $someProp={someProp}  // '$' signals the transient prop        
    >
        {children}
    </StyledPaneContainer>
);
Spiros answered 26/6, 2020 at 23:18 Comment(3)
I really wish the styled-components documentation had been updated to include thisSciomancy
Updated the documentation in answer - styled-components.com/docs/api#transient-propsShopper
This should be the accepte answer.Behoof
P
49

As suggested by vdanchenkov on this styled-components github issue you can destructure the props and only pass the relevant ones to Resizable

const ResizableSC = styled(({ someProp, ...rest }) => <Resizable {...rest} />)`
    flex: 0 0 ${(props) => props.someProp}px;
`;
Punkie answered 14/4, 2018 at 19:59 Comment(2)
you'll have to import react into the file where this is written to make this workCardiff
I believe this will also kill any as="foo" type props tooHypercriticism
D
12

For those (like me) that landed here because of an issue with SC and react-router's Link.

This is basically the same answer as @tskjetne, but with JS syntax style.

const StyledLink = styled(({ isCurrent, ...rest }) => <Link {...rest} />)(
  ({ isCurrent }) => ({
    borderBottomColor: isCurrent ? 'green' : 'transparent',
  }),
)
Devitt answered 9/3, 2019 at 16:47 Comment(2)
How didn't I think of this? Thank you so much!Shevat
If we have to use it like this, styled-components doesn't make that much sense anymore. It could have been a nice `styled(Link)```...``but not.Diatomaceous
E
3

Starting with version 5.1, you can use shouldForwardProp configuration property:

This is a more dynamic, granular filtering mechanism than transient props. It's handy in situations where multiple higher-order components are being composed together and happen to share the same prop name. shouldForwardProp works much like the predicate callback of Array.filter. A prop that fails the test isn't passed down to underlying components, just like a transient prop.

Keep in mind that, as in this example, other chainable methods should always be executed after .withConfig.

I find it much cleaner than transient props, because you don't have to rename a property, and you become explicit about your intentions:

const ResizableSC = styled(Resizable).withConfig({
  // Filter out the props to not be shown in DOM.
  shouldForwardProp: (prop, defaultValidatorFn) =>
    prop !== 'someProp'
    && defaultValidatorFn(prop),
})`
  flex: 0 0 ${(props) => props.someProp}px;
`;

This is especially handy if you are using TypeScript and sharing the same props type both in your main component and the corresponding styled component:

import { HTMLAttributes } from 'react';
import styled from 'styled-components';

// Props type.
type CaptionProps = HTMLAttributes<HTMLParagraphElement> & {
  size: number,
};

// Main component.
export const CaptionStyles = styled('p').withConfig<CaptionProps>({
  // Filter out the props to not be shown in DOM.
  shouldForwardProp: (prop, defaultValidatorFn) => (
    prop !== 'size'
    && defaultValidatorFn(prop)
  ),
})`
  flex: 0 0 ${(props) => props.size}px;
`;

// Corresponding styled component.
export function Caption({ size }: CaptionProps) {
  return (
    <CaptionStyles size={size} />
  );
}

shouldForwardProp API reference

Eichelberger answered 11/11, 2022 at 11:45 Comment(0)
H
-1

You can try with defaultProps:

import Resizable from 're-resizable';
import PropTypes from 'prop-types';
...
const ResizableSC = styled(Resizable)``;
export const StyledPaneContainer = ResizableSC.extend`
    flex: 0 0 ${(props) => props.someProp}px;
`;

StyledPaneContainer.defaultProps = { someProp: 1 }

const PaneContainer = ({ children }) => (
    <StyledPaneContainer>
        {children}
    </StyledPaneContainer>
);

export default PaneContainer;

We can also pass props using 'attrs'. This helps in attaching additional props (Example taken from styled components official doc):

const Input = styled.input.attrs({
  // we can define static props
  type: 'password',

  // or we can define dynamic ones
  margin: props => props.size || '1em',
  padding: props => props.size || '1em'
})`
  color: palevioletred;
  font-size: 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;

  /* here we use the dynamically computed props */
  margin: ${props => props.margin};
  padding: ${props => props.padding};
`;

render(
  <div>
    <Input placeholder="A small text input" size="1em" />
    <br />
    <Input placeholder="A bigger text input" size="2em" />
  </div>
); 
Hangout answered 14/4, 2018 at 17:50 Comment(5)
Could you explain this answer?Soapstone
You can pass default props to the component StyledPaneContainer using defaultProps. Did you try this out?Hangout
No, I wanted to understand why defaultProps was suggested if the value of someProp needs to be dynamic (?)Soapstone
In that we can try with 'attrs'. Updating my answer. Please check, let me know your thoughts on the same.Hangout
Unfortunately attrs passes all props through to the DOM, so it doesn't prevent React's invalid props warning unless you're only using prop names that also happen to be valid HTML attributes. I hope styled-components improves its API in the future to better support this use case. In the meantime @tskjetne's answer is a viable workaround for most cases.Trunkfish

© 2022 - 2024 — McMap. All rights reserved.