How do Styled Components forward props? - javascript

I have been working with Styled Components for a while now but I'll be honest I never really understood how it works. That is what my question is about.
So, I understand styled components can adapt based on props. What I don't understand is how does the forwarding of props work.
For Example in my case, Im working with React Native. There are some default props which I have given to my input field. Now, styled component wrapper automatically picks up the default height prop but If I explicitly pass the prop it does not pick it up and I have to manually get it from props. What is that about?
import React from "react";
import styled from "styled-components/native";
const StyledTextInput = styled.TextInput`
/* Why do I have to do this for custom heights.
* Isn't height automatically get forwarded?
*/
/* if I comment this height style out, It takes defaultProp height but doesn't take custom height
* I have to uncomment it for the custom height
*/
height: ${({ height }) => height};
...other styles
`;
const TextInput = ({ height }) => {
return <StyledTextInput height={height} />;
};
TextInput.defaultProps = {
height: 50
};
export default TextInput;
// Then In some Smart Component
class App extends Component {
render() {
return (
<View style={styles.app}>
<TextInput height={200} /> // ???
...other stuff
</View>
);
}
}
Here are my Questions:
What are the cases in which styled component automatically picks up the prop?
Which props are automatically forwarded.
How does this prop forwarding work?
Documentation doesn't talk much about that or maybe I have missed it.
Any help would be much appreciated.

All standard attributes are automatically forwarded props. Standard attributes like defaultValue and type. Take note of the camel case notation for attributes in React.
If it's a custom attribute like someCustomAttribute prop, it's not passed directly to DOM.
If there are props that are true to all instances of the styled component, you can make use of .attrs.
const StyledTextInput = styled.TextInput.attrs(props => ({
height: props.height
}))`
// other styles
`

Related

Why does custom input component cause "Function components cannot be given refs" warning?

While trying to customize the input component via MUI's InputUnstyled component (or any other unstyled component, e.g. SwitchUnstyled, SelectUnstyled etc.), I get the warning
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
Check the render method of `ForwardRef`.
InputElement#http://localhost:3000/main.4c2d885b9953394bb5ec.hot-update.js:59:45
div
...
I use the components prop to define a custom Input element in my own MyStyledInput component which wraps MUIs InputUnstyled:
import InputUnstyled, {
InputUnstyledProps
} from '#mui/base/InputUnstyled';
const MyStyledInput: React.FC<InputUnstyledProps> = props => {
const { components, ...otherProps } = props;
return (
<InputUnstyled
components={{
Input: InputElement,
...components,
}}
{...otherProps}
/>
);
};
My custom input component InputElement which is causing the Function components cannot be given refs warning:
import {
InputUnstyledInputSlotProps,
} from '#mui/base/InputUnstyled';
import { Box, BoxProps } from '#mui/material';
const InputElement: React.FC<BoxProps & InputUnstyledInputSlotProps> = (
{ ownerState, ...props }
) => {
return (
<Box
component="input"
// any style customizations
{...props}
ref={ref}
/>
);
});
Note: I'm using component="input to make MUI's Box component not render an HTML div but an HTML input component in the DOM.
Why am I getting this warning?
Other related questions here, here and here address similar issues but don't work with MUI Unstyled components. These threads also don't explain why
The warning wants you to have a look at the InputElement component. To be honest, the stack-trace is a bit misleading here. It says:
Check the render method of ForwardRef.
InputElement#http://localhost:3000/main.4c2d885b9953394bb5ec.hot-update.js:59:45
div
You can ignore the ForwardRef here. Internally InputElement is wrapped by
The crucial part for understanding this warning is:
Function components cannot be given refs. Attempts to access this ref will fail.
That is, if someone tries to access the actual HTML input element in the DOM via a ref (which Material UI actually tries to do), it will not succeed because the functional component InputElement is not able to pass that ref on to the input element (here created via a MUI Box component).
Hence, the warning continues with:
Did you mean to use React.forwardRef()?
This proposes the solution to wrap your function component with React.forwardRef. forwardRef gives you the possibility to get hold of the ref and pass it on to the actual input component (which in this case is the Box component with the prop component="input"). It should look as such:
import {
InputUnstyledInputSlotProps,
} from '#mui/base/InputUnstyled';
import { Box, BoxProps } from '#mui/material';
const InputElement = React.forwardRef<
HTMLInputElement,
BoxProps & InputUnstyledInputSlotProps
>(({ ownerState, ...props }, ref) => {
const theme = useTheme();
return (
<Box
component="input"
// any style customizations
{...props}
ref={ref}
/>
);
});
Why do I have to deal with the ref in the first place?
In case of an HTML input element, there as a high probability you want to access it's DOM node via a ref. This is the case if you use your React input component as an uncontrolled component. An uncontrolled input component holds its state (i.e. whatever the user enters in that input field) in the actual DOM node and not inside of the state value of a React.useState hook. If you control the input value via a React.useState hook, you're using the input as a controlled component.
Note: An input with type="file" is always an uncontrolled component. This is explained in the React docs section about the file input tag.

React styled-components style a dynamic component

I have a following problem:
I have defined some icons in my project. Each icon is defined by a component type (an icon from Material-UI) plus some additional props. I did it that way, so I can then render those icons in differing ways.
Anyway, let's say that I want to re-use the same icon in 2 places, but in one place I want it to be red, in the other I want it to be green. MUI icons don't accept a color prop (I mean not an RGB one, just a few predefined colors). So the way to do it is to style the icon component with css (color:).
I use styled-components so the way I would usually do this is:
const StyledIcon = styled(AssignmentIcon)`
color: red
`
But the thing is, in some places I don't know which icon I'll be styling. For example some component may receive a list with AssignmentIcons and BugReportIcons. As it is with styled-components, I cannot define the styled components inside another component - so I cannot create a style like the one above, and just pass icons from the props to it.
I solved it like this: https://codesandbox.io/s/nostalgic-hofstadter-ww2ly. Basically, I have a styleIcon function, which takes a React.ComponentType (dynamically of course), puts in in styled() and returns it. This way I've applied some style to the icon and now I can render it.
My question is: this seems kinda sketchy, having some helper functions to do the job. Is there a simpler way to do it? (and by the way, I cannot use style={{}} props when rendering the icon - it may collide with other styles that I would apply via prop-spreading)
Your component should always be able to switch to different color.
const color = props => props.color || 'red'
const StyledIcon = styled.div`
color: ${color};
`
If the color is given, then use color, ex.
return <StyledIcon color="blue" />
I don't know if this solves your problem. Sometimes we want to use a theme mode, for example StyledComponent does support a theme.
https://styled-components.com/docs/advanced
Even when you don't want to always use one theme, you can apply different theme at run time.
<ThemeProvider theme={theme1}>
// A
<ThemeProvider theme={theme2}>
// B
<Button>Themed</Button>
</ThemeProvider>
// C
</ThemeProvider>
// D
The locations A, B, C, D gets different themes:
A. theme1
B. theme2
C. theme1
D. defaultTheme
I believe the base solution should give u some ideas, but if not, try this sophisticated theme options offered by StyledComponent.
Based on your problem, I assume that you want to change the color from Material UI without explicitly styling. So I came up with a wrapper that takes Material UI icons as one child node.
First, we need to create the styling for that wrapper:
const StyledIcon = styled.span<{color: string}>`
svg, .icon {
color: ${props => props.color}; // You can even pass a hex color
}
`
Secondly, create a separate React component .You use can the styled one alone but this will help you with handling states and effects in the future:
interface ColorfulIconProps {
color: string;
[k: string]: any;
}
const ColorfulIcon: React.FC<ColorfulIconProps> = ({ color, children, ...rest }) => {
// States and effects
return (
<StyledIcon color={color} {...rest}>
{children}
</StyledIcon>
);
};
Finally, to use it, just simply pass any Material UI icons you want, or your own icon set as long as they're formatted in svg
const App = () => {
return (
<ColorfulIcon color="red">
<AMaterialUIIcon />
</ColorfulIcon>
);
};
Live demo:
https://codesandbox.io/s/modern-brook-oecgn?file=/src/StyledIcon.tsx

How to use the key attribute do in an img tag?

I have this code to make an array of images to display on the screen. However, I don't know what the key attribute in the images array means. Ideally I want to change the src of the image whenever one of the images is clicked. If I add in an id or className property to the image, and look it up using document.getElementById i get this warning: Warning: Prop `%s` did not match. Server: %s Client: %s%s when I rendered the page. I am using react and razzle for this project. Can someone tell me how to accomplish this?
var shuffle = require("shuffle-array"),
animals = [
"frog0",
"frog1",
// ...
"seal1",
"armadillo0",
"armadillo1"
];
function whatami(img) {
alert(img);
}
var images = shuffle(animals).map(image => {
return (
<img
onClick={() => {
whatami(image);
}}
key={image}
src={"/animalgameback.jpg"}
/>
);
});
const App = () => (
<div>
<h3>My razzle app</h3>
<div>{images}</div>
</div>
);
export default App;
There quite a few things wrong with your approaches here. I would highly suggest reading through the official React documentation on how to write Javascript in ReactJS.
Let's cover some basics. Firstly, you really should not ever be using document.getElementById in React (unless the situation is dire and you are trying to hack apart a third party library). For the most part, you use the prop ref to reference React Nodes that are mounted in the DOM. But, just some advice for those learning, have your fun with using references so that you know how to use them and what they do. But. I would suggest that if you "need" a reference or "need" to directly talk to a React component at run-time, you might be doing something wrong.
Now, since you are attempting to "change" something based on user events or interactions, this is a perfect use-case for state management. React comes with the ability for each component to self-encapsulate a stateful object and use these states to "trigger" or re-render things in components, due to this update or change.
What is a key? It is a unique identifier that you can use on each rendered JSX component that shows the virtual DOM and the real DOM, that this component is intended to be re-rendered as is rather than unmounted, changed and re-mounted. A key allows React to keep track of which components were intended versus just respawned or mounted. You always write a key of a JSX element as a unique id. If you made 2 id's the same (try it out and see :) ) you would notice that they render as 1 on the screen and one replaces the other.
Heres how I would write this:
I have made a single image as a "viewer" to show which image was clicked, along with a click event handler attached to the image to change the state. The render function detects the image source change and re-renders the component. Therefore, the new source is received and used and rendered.
The Code
import React, { Component } from 'react';
const ANIMAL_DATA = [
'frog0','frog1','sheep0','sheep1','snail0','snail1','mouse0','mouse1','bat0','bat1','walrus0',
'walrus1','giraffe0','giraffe1','zebra0','zebra1','dog0','dog1','octopus0','octopus1','hippo0',
'hippo1','camel0','camel1','pig0','pig1','rhino0','rhino1','rooster0','rooster1','panda0','panda1',
'turtle0','turtle1','raccoon0','raccoon1','polarbear0','polarbear1','lion0','lion1','bison0',
'bison1','orca0','orca1','snake0','snake1','shark0','shark1','toucan0','toucan1','butterfly0',
'butterfly1','anteater0','anteater1','seal0','seal1','armadillo0','armadillo1'
]
class App extends Component {
constructor(props) {
super(props);
this.state = {
imageSource: 'animalgameback',
};
}
render() {
const { imageSource } = this.state;
return (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<img style={{ width: 143, height: 'auto' }} source={require(`/${imageSource}.jpg`)} />
{ this.renderAnimalImages() }
</div>
);
}
renderAnimalImages() {
let images = [];
ANIMAL_DATA.forEach((animal, animalIndex) => {
// Be careful when assigning "onclick" methods to images,
// you are better off sticking with W3C rules on this. Use elements
// meant for "clicking" or "being clicked", i.e. <a>, <button>, etc
images.push(
<img
src={`/${animal}.jpg`}
key={`anima_image_${animalIndex}`}
onClick={this.__handleImageClick(animal)} />
);
});
return images;
}
__handleImageClick = animal => event => {
this.setState({ imageSource: animal });
};
}
export default App;
The key attribute serves as an identity declaration. It helps rendering engine to decide which elements should be re-rendered.
It's well explained in the documentation.

Style a styled-components within a wrapper

I would like to add an animation and a specific height to a Button that is styled. The thing is, my StyledButton is a wrapper that can render one of multiple pre-styled buttons according to a type prop which are styled React Semantic UI Buttons.
See the CodeSandbox with reproduction here :
https://codesandbox.io/embed/practical-haibt-oz9sl
The thing is it does get the styles from the ActionButton but it does not apply whatever style I put on the const AnimatedButton = styled(StyledButton).
But, if I try the same thing without the wrapper, directly by importing the BaseButton, and creating a AnimatedBaseButton, this one works but
removes the modularity of having a type prop that returns a pre-styled button.
I searched here and on google / github, but there's no issue that reflects this one. I know I could add an animation property on the StyledButton and pass it, but with the real codebase, it's not possible.
Thanks in advance !
EDIT : Added a Codesandbox instead of code example.
Quick fix:
In StyledButton.js:
render() {
const {
content,
icon,
iconPosition,
onClick,
type,
...otherProps // take base props passed through wrapper
} = this.props;
// ...
return (
<ButtonToDisplay
{...otherProps} // spread it firstly here so below props can override
onClick={onClick}
content={content}
/>
);
}
Why it works:
As you can see, styled(comp)'' syntax we use to style our component is actually a HOC component under the hood, which takes in a component and returns another component.
So when you make a wrapper that intercepts between a styled component and the real component, you need to allow props that generated by the library go through that wrapper.
You forgot the ... (spread operator) while destructing this.props
export default class StyledButton extends React.Component {
render() {
// added ... (spread operator)
const {type, ...additionalProps} = this.props
if (type === 'normal') return <NormalButton {...aditionalProps} />
else if (type === 'action') return <ActionButton {...aditionalProps} />
}
}
What is happening here is that styled-component pass the styles in the style prop, but with out the spread operator, you aren't passing it, you are just getting a prop that is called additionalProps.

Can I make styles apply to classes other than the first defined in a file?

I have inherited some code and I've been manipulating it, but I came across something that makes me scratch my head.
I am not sure whether the issue I am having relates to specifically to react.js or more generally to CSS / javascript...
In this code, we make use of react.js withStyles.
I have set up a sandbox to demonstrate.
First thing in the file, we define some styles that can then be applied to page elements, e.g.
const styles = theme => ({
buttonGroup: {
width: "250px",
display: "flex",
flexWrap: "wrap",
}, ...
then, when we define a class, we can get access to these styles by doing a const { classes } = this.props , e.g.
class MyButtons extends React.Component {
render() {
const { classes } = this.props;
return (
<div className={classes.buttonGroup}>
{three_buttons.map(e => (
<Button className={classes.a_button}>{e}</Button>
))}
</div>
);
}
}
That is all fine and works.
What I've tried is to, in the same file, define a second class, which I then call from the first (to make a component within a component).
However, the const { classes } = this.props does not seem to gain me access to the styles defined at the top, and when I try to set className={classes.buttonGroup} in the second class, I get an error
TypeError: read property 'buttonGroup' of undefined
I am pretty sure I can overcome this by simply defining the second class in a separate file like I usually do, but I would like to understand why this does not work.
You are not passing the styles as props to MyOtherButtons Component and hence you are getting this issue. Pass the classes as props and things would work as expected. It works for MyButtons component since you are passing the styles using withStyles syntax.
Check the working link https://codesandbox.io/s/m3rl6o2qyj

Categories