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.
Related
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.
Let's say I have a component with a scrollable subcomponent, and I want to expose the ability to scroll:
const MyComponent = (props) => {
return <ScrollView ... />
}
I want to be able to do
<MyComponent ref={myRef} />
...
myRef.scrollTo({x: 0});
So I need a way to forward the ref to the <ScrollView>. Let's try putting the ref on the props:
const MyComponent = (props) => {
return <ScrollView ref={props.scrollRef} ... />
}
...
<MyComponent scrollRef={myRef} />
...
myRef.scrollTo({x: 0});
I just tried that with React Native on iOS, and it indeed works. I see several advantages over React.forwardRef:
Simpler, because I don't need to use another React API.
Works also if there is more than one child who needs ref forwarding.
Seems to me that this approach is
What's the advantage of React.forwardRef? Why was it added in React 16.3?
Note that there is no difference between using another named prop like innerRef FOR FORWARDING, it works the same.
Refactoring class components
Since React moved toward function components (hooks) you might want to refactor the class component code to a function component without breaking the API.
// Refactor class component API to function component using forwardRef
<Component ref={myRef} />
React.forwardRef will be your only option (further explained in details).
Clean API
As a library author you may want a predictable API for ref forwarding.
For example, if you implemented a Component and someone wants to attach a ref to it, he has two options depending on your API:
<Component innerRef={myRef} />
The developer needs to be aware there is a custom prop for forwarding
To which element the innerRef attached? We can't know, should be mentioned in the API or we console.log(myRef.current)
<Component ref={myRef} />
Default behavior similar to ref prop used on HTML elements, commonly attached to the inner wrapper component.
Notice that React.forwardRef can be used for function component and HOC (for class component see alternative below).
Ref forwarding is not limited to DOM components. You can forward refs to class component instances, too.
For function components, forwardRef sometimes comes with useImperativeHandle combo (in class component you just call the class methods on ref instance: ref.current.myAttr().
// Same usage
<Component ref={myRef} />
const Component = React.forwardRef((props, ref) => {
// you can forward ref <div ref={ref} />
// you can add custom attributes to ref instance with `useImperativeHandle`
// like having ref.myAttribute() in addition to ones attached to other component.
});
Important behavior of ref prop without forwardRef.
For the class component, this code alone will attach the ref to CLASS INSTANCE which is not useful by itself and need another ref for forwarding:
// usage, passing a ref instance myRef to class Component
<Component ref={myRef} />
Full example, check the logs:
// We want to forward ref to inner div
class ClassComponent extends React.Component {
innerRef = React.createRef();
render() {
// Notice that you can't just `this.props.ref.current = node`
// You don't have `ref` prop, it always `undefined`.
return <div ref={this.innerRef}>Hello</div>;
}
}
const Component = () => {
const ref = React.useRef();
useEffect(() => {
// The ref attached to class instance
console.log(ref.current);
// Access inner div through another ref
console.log(ref.current.innerRef);
}, []);
return <ClassComponent ref={ref} />;
};
In function components, it won't even work because functions don't have instances.
By default, you may not use the ref attribute on function components because they don’t have instances. [1]
forwardRef.
Refs and the DOM.
Why we need ref forwarding?
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
`
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
I need to remove a prop from a child.
I have a container element which uses a property on it's children to perform some enhancements on the children. That property should be removed from the child before rendering.
<AsyncContainer>
<Button onClick={this.asyncStuff} asyncHandler="onClick"/>
</AsyncContainer>
The asyncHandler property should be removed from the button before rendering.
AsyncContainer uses React.cloneElement(child, properties).
I've tried nulling the asyncHandler property, setting it to undefined and deleting the property from the child.props. It seems that it is impossible to get rid of this property again.
I just ran into this issue. You can just create a new element and use the old element's type and props you want to pass through. I'm not sure if this an anti-pattern or not, I just stumbled on it and it seems to be working well so far.
It should look something like this:
function AsyncContainer(props) {
const child = React.Children.only(props.children)
const { asyncHandler, ...childProps } = child.props
// do asyncHandler stuff
return React.createElement(child.type, childProps)
}
function AsyncContainer(props) {
const child = React.Children.only(props.children);
return React.cloneElement(
child,
{ asyncHandler: undefined }
);
}
How it works
You clone element using React.cloneElement because element is immutable and only way to change its props is to create clone.
Use second React.cloneElement argument to add new props and remove old props. Unneeded props should be assigned with undefined. You need to do this because by default cloned element is cloned with all its props.
As per the comments you cannot modify the props directly as they are immutable.
However, I think I have a simple solution to this problem. I have no idea what library that is or how it works, so this may or may not work. However, this is a general answer to how you would remove a prop before a component gets mounted.
That being said, I would try to create my own component which renders a <Button />:
class MyButtonComponent extends React.Component {
...
render() {
return <Button onClick={this.props.onClickHandler} />;
}
}
Then in the component you want to do your enhancements:
render() {
<AsyncContainer>
<MyButtonComponent onClickHandler={this.asyncStuff} asyncHandler="onClick"/>
</AsyncContainer>
}
This way you maintain your onClick eventlistener on the <Button /> component but you don't pass the illegal asyncHandler prop.
Edit:
Alternatively, you could also do:
class MyButtonComponent extends React.Component {
...
componentWillMount() {
let newProps = this.props;
delete newProps.asyncHandler;
this.setState({properties: newProps}):
}
render() {
return <Button {...this.state.properties} />;
}
}
This will apply all the props (with the spread operator) to <Button /> except for asyncHandler which we delete prior to the component being mounted by creating a copy of the props in state but with asyncHandler removed.
Also check this answer I gave to a similar question.