I'm trying (unsuccessfully) to pass a single color value from a parent to a child using styled-components.
If the color is passed correctly, the background should be the color that is passed. Else green.
No matter what I do, the background-color is green
What am I missing here?
The parent:
function App() {
return (
<div>
<Article color="red"/>
</div>
);
}
The child:
const MainContetnt = styled.div`
background-color: ${props => props.color ?? "green"};
flex: 1;
`;
const Article = (props) => {
return (
<MainContetnt>
Main content
</MainContetnt>
)
};
Props will not be passed automatically to a styled component, you still have to do it the usual way:
const Article = (props) => {
return (
<MainContetnt color="props.color">
Main content
</MainContetnt>
)
};
Related
I would like to create a Portal component that is supposed to be attached to it's container component, but not via the container's ID but by it's ref. In other words, I don't want to pass document.getElementById('CONTAINER_ID') as the second argument to the ReactDOM.createPortal function but to rely solely on the ref of the container being passed by React.forwardRef.
Is there a straightforward method of achieving this ?
Else maybe I would need to create a dom node "attached" to the ref first and than pass the node to the
createPortal function as the second argument ? I would like to avoid assigning the ids as much as possible. Here is the example code (I work in TypeScript) :
export default React.forwardRef<HTMLDivElement, Props>( (Props, ref) =>{
const {state, options, dispatch} = Props
if(!state.open) return null
return ReactDOM.createPortal(
<div
className={css`
height: 140px;
background: white;
overflow-y: scroll;
position: absolute;
width:100%;
`}
>
{options}
</div>,
ref.current // <-- THIS DOESN'T WORK
)
}
)
forwardRef is used when the parent component needs to get access to the child's element, not the other way around. To pass an element down to a child you'll do it through a prop. Also, since refs aren't populated until after the first render, you may need to render the parent component twice, once to put the container on the page, and then again to pass it to the child so the child can create a portal.
const Parent = () => {
const ref = useRef<HTMLDivElement>(null);
const [element, setElement] = useState<HTMLDivElement | null>(null);
useEffect(() => {
// Force a rerender, so it can be passed to the child.
// If this causes an unwanted flicker, use useLayoutEffect instead
setElement(ref.current);
}, []);
return (
<div ref={ref}>
{element && (
<Child element={element} {/* insert other props here */} />
)}
</div>
)
}
const Child = (props: Props) => {
const { state, options, dispatch, element } = props;
if (!state.open) {
return null;
}
return ReactDOM.createPortal(
<div
className={css`
height: 140px;
background: white;
overflow-y: scroll;
position: absolute;
width: 100%;
`}
>
{options}
</div>,
element
);
}
Let's say I have this:
const Child = styled.button`
background-color: grey;
`;
<div className="some-non-styled-class">
<Child>Hello World</Child>
</div>;
<div className="some-other-non-styled-class">
<Child>Hello World</Child>
</div>;
How can I style Child so it's "red" when under some-non-styled-class and "blue" when it's under some-other-non-styled-class?
I'm aware of the ability to refer to other styled-components, but I have a use case where I need to change styling based on an existing library with static class names.
You could try something like this then:
export const Child = () => {
const [parentClass, setParentClass] = useState("")
useEffect(() => {
const currentClass = document.querySelector(".childComp").parentElement.className
setParentClass(currentClass)
},[])
return (<MyStyledComponent className="childComp" parentClassName={parentClass}>
{children}
</MyStyledComponent>)
}
I want to split the components into baseUI one and styled one:
eg.
MyComponent.jsx
export default class MyComponent extends React.Component {
...
...
render() {
const { wrapperClassName, className, childClassName } = this.props;
return (
<div className={wrapperClassName>
<div className={className />
<div className={childClassName} />
</div>
)
}
}
StyledMyComponent.jsx
import styled from 'react-emotion'
const StyledMyComponent = styled(MyComponent)(
...
...
)
export default StyledMyComponent
however anything I put to the styled function's argument they will go to the className only, is there a way I specify which props goes to which className?
also can I do something like sass/less with children selector?
hypothetically something like this:
const classes = css`
color: red;
span { // this works
color: black;
}
.childClassName { // this doesn't work
color: green;
}
`
<MyComponent className={classes} />
No you can't.
What you can do, is create specific components for the underlying div. This is how I make my components:
const MyComponentStyle = styled('div')....;
const MySecondComponentStyle = styled('div')...;
const MyThirdStyle = styled('div')...;
const MyComponent = ({ wrapperClassName, childClassName, className }) =>
<MyComponentStyle className={wrapperClassName}>
<MySecondComponentStyle className={className} />
<MyThirdStyle className={childClassName} />
</MyComponentStyle>
)
}
}
Conditionally styling the element and its children based on class names
You can conditionally change the styling of stuff below the main component based on its classes.
Taking your example:
const Something = () => (
<MyComponent className={classes}>
<div className="childClassName">child</div>
<div className="otherChildClassName">child</div>
</MyComponent>
You can style the children like so:
const classes = css`
color: red;
span {
color: black;
}
& .childClassName {
color: green;
}
`
note the & character. It essentially means "this class". So & .childClassName means "childrens of this element with class childClassName.
You could also use &.someClassName (note the lack of space), which would mean: "this element when it also has a class named someClassName.
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 (?)
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>
);
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;
`;
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',
}),
)
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
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>
);
I have the following styled component:
const Component = styled.div`
...
`;
const Button = (props) => {
return (
<Component>
...
</Component>
);
};
export default styled(Button)``;
I want to get a reference to the underlying div of Component. When I do the following I get null:
import Button from './Button.js';
class Foo extends React.Component {
getRef = () => {
console.log(this.btn);
}
render() {
return (
<Button innerRef={elem => this.btn = elem} />
);
}
}
Any ideas why I am getting null and any suggestions on how to access the underlying div?
Note: The reason I am doing this export default styled(Button)``; is so that the export styled component can be easily extended.
I managed to accomplish this by passing a function down as a prop to the styled-component that I was targeting, then passing the ref back as an argument of the function:
const Component = styled.div`
...
`;
const Button = (props) => {
return (
<Component innerRef={elem => props.getRef(elem)}>
...
</Component>
);
};
export default styled(Button)``;
...
import Button from './Button.js';
class Foo extends React.Component {
getRef = (ref) => {
console.log(ref);
}
render() {
return (
<Button getRef={this.getRef} />
);
}
}
Passing a ref prop to a styled component will give you an instance of the StyledComponent wrapper, but not to the underlying DOM node. This is due to how refs work. So it's not possible to call DOM methods, like focus, on styled components wrappers directly.
Thus to get a ref to the actual, wrapped inner DOM node, callback is passed to the innerRef prop as shown in example below to focus the 'input' element wrapped inside Styled component 'Input' on hover.
const Input = styled.input`
padding: 0.5em;
margin: 0.5em;
color: palevioletred;
background: papayawhip;
border: none;
border-radius: 3px;
`;
class Form extends React.Component {
render() {
return (
<Input
placeholder="Hover here..."
innerRef={x => { this.input = x }}
onMouseEnter={() => this.input.focus()}
/>
);
}
}
render(
<Form />
);
NOTE:-
String refs not supported (i.e. innerRef="node"), since they're already deprecated in React.