How to prevent a prop from being passed to the extended component? - reactjs

My question is similar to this one, however I need to type a component. I have tried a multiple ways, but still getting errors.
I am not sure if the problem is in my understanding, or am I doing something wrong? Here is what I tried:
First approach works correctly but throws warning: React does not recognize the isActive prop on a DOM element
Second and Third throws TS error: No overload matches this call.
import * as React from "react";
import TextareaAutosize, { TextareaAutosizeProps } from "react-textarea-autosize";
import styled from "styled-components";
interface Props {
isActive?: boolean;
}
interface ExtendedProps extends Props, TextareaAutosizeProps {}
const FirstTry = styled(TextareaAutosize)<Props>`
color: ${({ isActive }) => (isActive ? "red" : "blue")};
`;
const SecondTry = styled(({ isActive, ...rest }: ExtendedProps) => (
<TextareaAutosize {...rest} />
))`
color: ${({ isActive }) => (isActive ? "red" : "blue")};
`;
const ThirdTry = ({ isActive, ...rest }: ExtendedProps) => {
const Foo = styled(TextareaAutosize)<TextareaAutosizeProps>`
color: ${isActive ? "red" : "blue"};
`;
return <Foo {...rest} />;
};
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<FirstTry isActive minRows={3} />
{/* FirstTry - Warning: React does not recognize the `isActive` prop on a DOM element */}
<SecondTry isActive minRows={3} />
<ThirdTry isActive minRows={3} />
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
Link to sandbox:
https://codesandbox.io/s/zen-cdn-lp8pl?file=/src/App.tsx

Your second approach looks good except for one small thing that causes the error: ref prop of TextareaAutosizeProps collides with ref prop of your styled component.
ref (and key for that matter) is a tricky "prop" - you pass it to a component just like any other props yet it does not appear in your props (if you log them for example), it is handled differently.
If you look at your second example:
const SecondTry = styled(({ isActive, ...rest }: ExtendedProps) => (
<TextareaAutosize {...rest} />
))`
color: ${({ isActive }) => (isActive ? "red" : "blue")};
`;
You can see that you are not styling TextareaAutosize but the anonymous function component ({ isActive, ...rest }: ExtendedProps) => .... If you pass ref to your SecondTry component it will not appear in your props ({ isActive, ...rest }: ExtendedProps). Yet when extending TextareaAutosizeProps you are also saying that there will be such a prop and it will be of type HTMLTextAreaElement.
So I can think of two solutions depending on your needs:
If you don't need the ref prop on your SecondTry you can just omit it from your props:
interface ExtendedProps extends Props, Omit<TextareaAutosizeProps, 'ref'> {}
If you need it though you will need to use React.forwardRef function (see more about that here).

Related

Is there a way to inject a SCSS class into a React component?

I've built a simple Panel.tsx component in React, using TypeScript:
import * as React from 'react';
import { Label, LabelType } from 'components/basic';
import styles from './Panel.module.scss';
interface IPanel {
title?: string;
children: React.ReactNode;
}
const Panel = ({ title, children }: IPanel) => {
return (
<div className={styles.main}>
{title && <Label type={LabelType.Title} bold text={title} />}
{children}
</div>
);
};
export default Panel;
Here's the companion Panel.module.scss file:
.main {
border: 2px solid $black;
border-radius: 10px;
padding: 10px;
}
Now, if I want to inject a SCSS class into Panel, perhaps with a color, background-color, font-size, etc. is there a way to do that?
Yes, it is quite common to write components in such a way that they accept a className property (but which must be applied to the component root explicitly by the component author). E.g. all components from material-ui accept a classname (I'm not a big fan of material-ui but at least this aspect they got right).
You can either add it explicitly to your prop type
interface IPanel {
title?: string;
children: React.ReactNode;
className?: string;
}
...and use it in your component
import classnames from 'clsx'; // this is a popular package to combine classnames
const Panel = ({ title, children, className }: IPanel) => {
return (
<div className={classnames(styles.main, className)}>
{title && <Label type={LabelType.Title} bold text={title} />}
{children}
</div>
);
};
...or you can use one of the utility types from React. E.g. I often just allow all common HTML attributes plus children to be added as props (Yes, I know, my types are not overly precise)
import type { FunctionComponent, HTMLAttributes } from 'react';
export type CommonPropsFunctionComponent<TProps = {}> = FunctionComponent<
TProps & HTMLAttributes<Element>
>;
import { CommonPropsFunctionComponent } from './types';
interface IPanel {
title?: string;
}
const Panel: CommonPropsFunctionComponent<IPanel> = ({ title, children, className }) => {
return (
<div className={classnames(styles.main, className)}>
{title && <Label type={LabelType.Title} bold text={title} />}
{children}
</div>
);
};
Using the spread operator
You might be tempted to use it here; but beware: you must be careful to not overwrite your own local classname accidently.
To illustrate this, in this example:
local tabIndex can be overwritten
local className list cannot be overwitten, only extended
const SomeComponent= ({ children, className, ...rest }) => {
return (
<div tabIndex={0} {...rest} className={classnames(styles.root, className)}>
{children}
</div>
);
};

Can you pass custom props to Material-UI v5 `styled()` components?

For example, in styled-components you can do the following:
const Div = styled.div`
background: ${props => props.primary ? "palevioletred" : "white"};
color: ${props => props.primary ? "white" : "palevioletred"};
`;
render(
<section>
<Div>Normal</Div>
<Div primary>Primary</Div>
</section>
);
Can you get the same result with Material-UI v5 styled utility without adding overrides to the global theme object like the example in the styled() docs?
Yes!
The most basic example of the above would look like this in MUI v5:
const Div = styled("div")(({ primary }) => ({
backgroundColor: primary ? "palevioletred" : "white",
color: primary ? "white" : "palevioletred"
}));
render (
<section>
<Div>Normal</Div>
<Div primary>Primary!</Div>
<section>
);
However, as the React docs say:
The unknown-prop warning will fire if you attempt to render a DOM element with a prop that is not recognized by React as a legal DOM attribute/property. You should ensure that your DOM elements do not have spurious props floating around.
So MUI gave us the shouldForwardProp option to tell MUI whether it "should forward the prop" to the root node or not. The above example would look like this using that prop:
const Div = styled("div", {
shouldForwardProp: (prop) => prop !== "primary"
})(({ primary }) => ({
backgroundColor: primary ? "palevioletred" : "white",
color: primary ? "white" : "palevioletred"
}));
render (
<section>
<Div>Normal</Div>
<Div primary>Primary!</Div>
<section>
);
Explanation
The second argument to the styled function is an options object, one of the things it accepts is shouldForwardProp, which as the docs say, "Indicates whether the prop should be forwarded to the Component". So to remove the unknown prop warning from the console, we tell it not to pass our custom prop to the DOM element with shouldForwardProp: (prop) => prop !== "primary". Now we destructure this prop in the function call that returns our custom styles, and use it in those styles like we would any other function.
If you want to use the global theme styles here as well, just destructure it along with your custom prop(s), ie ({ primary, otherProp, thirdProp, theme }).
Working codesandbox.
MUI v5 styled API docs
Here is a fully-working MUI v5 TypeScript example where you can pass custom properties to a styled component:
import React from 'react';
import { Button, styled, Typography } from '#mui/material';
const PREFIX = 'NimbusButton';
const classes = {
root: `${PREFIX}-root`,
button: `${PREFIX}-button`
};
interface RootProps {
textColor?: 'primary' | 'secondary';
buttonTextColor?: 'primary' | 'secondary';
}
const Root = styled('div', {
shouldForwardProp: (prop) => prop !== 'textColor' && prop !== 'buttonTextColor',
name: 'MyThemeComponent',
slot: 'Root'
})<RootProps>(({ theme, textColor, buttonTextColor }) => ({
[`& .${classes.root}`]: {
color: textColor ? theme.palette.primary.main : theme.palette.secondary.main
},
[`& .${classes.button}`]: {
color: buttonTextColor ? theme.palette.primary.main : theme.palette.secondary.main
}
}));
type OwnProps = {
textColor: 'primary' | 'secondary';
buttonTextColor: 'primary' | 'secondary';
text?: string;
buttonText: string;
};
const CustomStyledButton: React.FC<OwnProps> = (props) => {
const { textColor, buttonTextColor, text, buttonText } = props;
return (
<Root className={classes.root} textColor={textColor} buttonTextColor={buttonTextColor}>
{text && <Typography variant={'body1'}>{text}</Typography>}
<Button className={classes.button}>{buttonText}</Button>
</Root>
);
};
export default CustomStyledButton;
If you are using TypeScript, I use utility function for it, so I always type prop names correctly:
export const shouldForwardProp = <CustomProps extends Record<string, unknown>>(
props: Array<keyof CustomProps>,
prop: PropertyKey,
): boolean => !props.includes(prop as string);
const MyComponent = styled('div', {
shouldForwardProp: (prop) => shouldForwardProp<MyComponentProps>(['isDisabled', 'bgColor'], prop),
})<MyComponentProps>(({ theme, isDisabled, size, bgColor }) => ({
...

React - Forwarding props with forwardRef

I want to define a LinkButton component which will behaving like a link and look like a button.
My basic implementation complains that I need to use React.forwardRef, however I'm unsure how to do this properly, especially with prop forwarding.
Even when I just type the props as any I still get a complaint from the system saying the props are invalid.
I would like to know how to properly use forwardRef in this (or any, for that matter) situation.
My attempt looks like this:
import React from 'react'
import styles from './styles'
import { Button, withStyles } from '#material-ui/core'
import { Link } from 'react-router-dom'
interface IProps {
classes: any
to: string
children: string
}
const NextLink = ({ to, ...rest }: any) => <Link to={to} {...rest} />
const LinkButton = React.forwardRef((props: any, ref: any) => {
const { classes, children, ...rest } = props // <-- ISSUE BECAUSE OF rest PROP
return (
<Button
ref={ref}
role='nextButton'
className={classes.root}
classes={{ disabled: classes.disabled }}
component={NextLink}
{...rest}
>
{children}
</Button>
)
})
LinkButton.displayName = 'LinkButton'
export default withStyles(styles)(LinkButton)
------------
It would be called like this:
<LinkButton to='/' color='secondary'>
{`Go Home`}
</LinkButton>
By wrapping NextLink in forwardRef as well I was able to solve this.
const NextLink = React.forwardRef(({ to, ...rest }: any, ref: any) => (
<Link ref={ref} to={to} {...rest} />
))
NextLink.displayName = 'NextLink'

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

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>
);

Higher Order Component in React [duplicate]

i am learning react at the moment. this is the link with the code - http://redux.js.org/docs/basics/ExampleTodoList.html
I am having a bit of difficulty understanding what's going on in this part of the code
const Link = ({ active, children, onClick }) => {
if (active) {
return <span>{children}</span>
}
return (
<a
href="#"
onClick={e => {
e.preventDefault()
onClick()
}}
>
{children}
</a>
)
}
Link.propTypes = {
active: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired
}
I am having the most difficulty understand this snippet
return (
<a
href="#"
onClick={e => {
e.preventDefault()
onClick()
}}
>
{children}
</a>
)
}
What does {children} mean here?
What does it do?
and what does this do?
children: PropTypes.node.isRequired,
what is meant by node in the above line?
When you use a Custom component, like
<MyComponent>Hello World</MyComponent>
Whatever you write between the tags (in above case Hello World) is passed to the component as a children prop.
So when write your component like
const Link = ({ active, children, onClick }) => {
You are destructuring the props and getting only active, children and onClick from the props passed to the component
Consider for example, you call the Link component like
<Link active="true" onClick={someFunc} style={{color: 'orange'}}>Hello</Link>
Then amongst all the props i.e active, onClick, style, children, you will only be accessing active, onClick,children in the component.
For your second question:
and what does this do?
children: PropTypes.node.isRequired,
So here PropTypes is a way of performing a typeCheck on the props that are passed to the component. It is being imported from the react-proptypes package.
So
children: PropTypes.node.isRequired
makes the prop children to be required. So if your render your component like
<Link />
It will not pass the type check and hence you need to do
<Link>Text</Link>
children: PropTypes.node.isRequired,
this is just the type checking of the react proptypes. Refer https://facebook.github.io/react/docs/typechecking-with-proptypes.html for more details how type checking works.
According to the example this says that the prop children is required and is of type node. This type node refers to anything that can be rendered. which is then included within the tag in your rendering.
If you care about the types of your props, you'd need to use React.PropsWithChildren, e.g.
type Props = React.PropsWithChildren<{
name: string; // strongly typed!
myProp: string;
}>;
export function MyComponent({ name, myProp, children }: Props) {
return (
<>
<div>{name}</div>
<div>{myProp}</div>
{children != null && children}
</>
)
}

Resources