I was able to put together that shouldForwardProp specifies which props should be forwarded to the wrapped element passed as an option to styled(), but I am having trouble finding a comprehensible example of its use case.
Is prop forwarding here akin to passing down props in React?
Why would one want to prevent certain props from being forwarded to the wrapped element while using styled()?
Forgive me for my ignorance or if my question lacks clarity - I am still learning MUI and attempting to wrap my head around it.
If you're using a built-in components like div or span and you want to allow the user to customize the styles via some props.
const MyComponent = styled('div')(({ bgColor }) => ({
backgroundColor: bgColor,
}));
When you're using it like this:
<MyComponent bgColor='red'>
The prop is passed to the real element in the DOM tree as attribute:
And react will complain, something like:
Warning: React does not recognize the `bgColor` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `bgcolor` instead. If you accidentally passed it from a parent component, remove it from the DOM element.
This is why shouldForwardProp exists, to prevent styling props from being passed down and create invalid attribute:
const MyComponent = styled('div', {
shouldForwardProp: (props) => props !== 'bgColor',
})(({ bgColor }) => ({
backgroundColor: bgColor,
}));
Already great answer by #NearHuscarl!
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 }) => ({
...
Related
I'm trying to implement a simple dark/light theme toggle to my website. In my base App.tsx I've implemented the state I use:
const [colorScheme, setColorScheme] = useState("light");
I pass that "colorScheme" variable as a prop to every other element. The theme toggle is contained in a header element, so I also pass the "setColorScheme" function to header as a prop. Within Header.tsx, the code triggered when the toggle is clicked is:
setColorScheme(s => s === "dark" ? "light" : "dark");
Within every specific element, I set the color scheme like so:
<ElementName className={"element_name element_name_"+colorScheme}/>
I have all the css for styling the component contained in the class "element_name", and then all relevant color data is contained in "element_name_light" or "element_name_dark".
When the toggle in the header is clicked, a re-render is triggered for the main body of the app, and for the header. But all of the other elements do not re-render. If I navigate to another element, the re-render happens and the color scheme appears as intended.
Attached is a gif of this happening.
I'm still learning React, so I'm sure it's something obvious I'm missing. I would appreciate any tips anyone can provide! Thanks
One note: I am using react functionally, rather than implementing classes for each component.
It's impossible to tell exactly what mistake you made since you haven't shared your code. But I can tell you the root mistake is not using React's context API. This will allow you to hold the color scheme and the toggle function as a global state and import them into every component via the useContext hook.
Here's an example on stackblitz: https://stackblitz.com/edit/react-ts-lhwstv?file=color-scheme-ctx.tsx
Here's the docs: https://reactjs.org/docs/context.html
Note: I'm using typescript, if you're using plain javascript just remove the type declarations and the generic typings <Type>.
You start by creating the context and giving a default value:
type ColorScheme = 'light' | 'dark';
type Props = { colorScheme: ColorScheme; toggleColorScheme: () => void };
export const ColorSchemeCtx = createContext<Props>({
colorScheme: 'light',
toggleColorScheme: () => {},
});
I like to then create a provider component for organization.
export const ColorSchemeCtxProvider: FC<PropsWithChildren<{}>> = ({
children,
}) => {
const [colorScheme, setColorScheme] = useState<ColorScheme>('light');
function toggleColorScheme() {
setColorScheme((s) => (s === 'dark' ? 'light' : 'dark'));
}
return (
<ColorSchemeCtx.Provider value={{ colorScheme, toggleColorScheme }}>
{children}
</ColorSchemeCtx.Provider>
);
};
Then wrap all components that need the context - probably just put it at the highest level possible.
root.render(
<StrictMode>
<ColorSchemeCtxProvider>
<App />
</ColorSchemeCtxProvider>
</StrictMode>
);
Now any component can get both the color scheme and / or the toggle function with useContext
export default function App() {
const { colorScheme, toggleColorScheme } = useContext(ColorSchemeCtx);
return (
<div>
<p>The color scheme is: {colorScheme}</p>
<button onClick={toggleColorScheme}>TOGGLE</button>
<CompOne />
<CompTwo />
<CompThree />
</div>
);
}
export default function CompOne() {
const { colorScheme } = useContext(ColorSchemeCtx);
return <div className={'comp-one ' + colorScheme}></div>;
}
I would like to only allow specific components as children. For example, let's say I have a Menu component, that should only contain MenuItem as children, like this:
<Menu>
<MenuItem />
<MenuItem />
</Menu>
So I would like Typescript to throw me an error in the IDE when I try to put another component as child. Something warning me that I should only use MenuItem as children. For example in this situation:
<Menu>
<div>My item</div>
<div>My item</div>
</Menu>
This thread is almost similar but does not include a TypeScript solution. I was wondering if the problem can be solved using TypeScript types and interfaces. In my imaginary world it would look like this, but of course the type checking is not working because the child component has an Element type:
type MenuItemType = typeof MenuItem;
interface IMenu {
children: MenuItemType[];
}
const MenuItem: React.FunctionComponent<IMenuItem> = ({ props }) => {
return (...)
}
const Menu: React.FunctionComponent<IMenu> = ({ props }) => {
return (
<nav>
{props.children}
</nav>
)
}
const App: React.FunctionComponent<IApp> = ({ props }) => {
return (
<Menu>
<MenuItem />
<MenuItem />
</Menu>
)
}
Is there a way to achieve this with Typescript? Like to extend the Element type with something related only to a specific component?
Or what would be a good approach for being sure that a child is an instance of a specific component? Without having to add condition that looks at the child component displayName.
To do that you need to extract props interface from children component (and preferably also parent) and use it this way:
interface ParentProps {
children: ReactElement<ChildrenProps> | Array<ReactElement<ChildrenProps>>;
}
so in your case it would look like this:
interface IMenu {
children: ReactElement<IMenuItem> | Array<ReactElement<IMenuItem>>;
}
const MenuItem: React.FunctionComponent<IMenuItem> = ({ props }) => {
return (...)
}
const Menu: React.FunctionComponent<IMenu> = ({ props }) => {
return (
<nav>
{props.children}
</nav>
)
}
Despite the answer above, you can't do this with children. You might do a runtime check in a dev build of your component, but you can't do this with TypeScript types — at least, not yet.
From TypeScript issue #18357:
Right now there's no way to specify what children representation is, except specifying ElementChildrenAttribute inside JSX namespace. It's heavily coupled with React representation for children which implies that children is a part of props. This makes impossible to enable type checking for children with implementations which store children separately, for instance https://github.com/dfilatov/vidom/wiki/Component-properties.
And note that it's referenced in #21699, where basically the possibly-breaking change around ReactElement may also make it possible to do this.
Right now, all you can do is that runtime check, or accept props (or arrays of props) and optionally a component function (in your case, you know it's MenuItem) and create the elements within your component.
There's also the question of whether you should do this. Why shouldn't I be able to write a component that returns a MenuItem rather than having to use MenuItem directly? :-)
I am beginner developer and I am working on react (gatsby, TS, styled components) project. I am getting this error:
"React does not recognize the isOpen prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase isopen instead. If you accidentally passed it from a parent component, remove it from the DOM element."
export const Navigation = () => {
const [isNavigationOpen, setIsNavigationOpen] = useState(false);
const { isTablet } = useQuery();
const showNavbar = () => {
setIsNavigationOpen((previousState) => !previousState);
};
const renderElement = isTablet ? (
<>
<SvgStyled
src='bars_icon'
isOpen={isNavigationOpen}
onClick={showNavbar}
/>
<MobileNavigation isOpen={isNavigationOpen}>
{NAVIGATION_DATA.map(({ id, url, text }) => (
<LinkMobile key={id} to={url}>
<ExtraSmallParagraph>{text}</ExtraSmallParagraph>
</LinkMobile>
))}
</MobileNavigation>
</>
) : (
<FlexWrapper>
{NAVIGATION_DATA.map(({ id, url, text }) => (
<LinkDekstop key={id} to={url}>
<ExtraSmallParagraph>{text}</ExtraSmallParagraph>
</LinkDekstop>
))}
</FlexWrapper>
);
return renderElement;
};
I am sure that I am missing some fundamental react stuff or something. Maybe someone could help me and explain the reason of this error.
When this happens it is because all props passed to the styled component are then also passed down to the DOM element that you are styling.
You've likely a component that looks like the following:
const SvgStyled = styled(SVG)<{ isOpen: boolean }>`
// your CSS and logic referencing the `isOpen` prop
`;
To resolve this issue you refactor the styled component definition and explicitly pass only the props you want to the element being styled. Use an anonymous function component and destructure the prop you don't want to pass on to the DOM element, and spread the rest of the props. This ensures the className prop that styled-components is creating a CSS class for is passed through.
Example:
interface SvgStyledProps {
className?: string,
isOpen: boolean,
}
const SvgStyled = styled(({ isOpen, ...props}) => (
<Svg {...props} />
))<SvgStyledProps>`
// your CSS and logic referencing the `isOpen` prop
`;
For any other Typescript specifics/caveats with styled-components see docs.
As of styled components v5.1, you can alternatively prevent undesired props from being passed down to your React node by prefixing it with a dollar sign ($) and designating it as a transient prop:
const SvgStyled = styled(SVG)<{ $isOpen: boolean }>`
// your CSS and logic referencing the `$isOpen` prop
`;
// SVG does NOT receive props.$isOpen
docs
I have a React project using MUI that currently has state for a theme (either light or dark). I am using the makeStyles/useStyles custom hook pattern in my component. I pass my theme state as a prop to the useStyles call in my component, but it does not update the component when theme changes. For example:
export const useStyles = makeStyles({
layout: ({ theme }: Props) => theme === LIGHT ?
lightObject... : darkObject...,
...rest
})
const Layout = ({ theme, children }: Props) => {
const { layout } = useStyles({ theme })
return (
<Box className={layout}>
{children}
</Box>
)
}
Calling setTheme in the component below (the parent of the component above), I assumed that because the parent component of this one changed state, it should have re-rendered the child. However, this is not the case. I found a "hacky" fix as shown below using the theme as a key:
// Theme, DARK, LIGHT are Typescript and Globals defined elsewhere
const App = () => {
const [theme, setTheme] = useState<Theme>(DARK)
const toggleTheme = () => setTheme(theme === LIGHT ? DARK : LIGHT)
return (
<div key={theme}>
<Layout theme={theme}>{some child...}</Layout>
</div>
);
}
When adding the key, I force the re-render. However, this again feels hacky. Is there a better way to do this? I don't really like using the ThemeProvider simply because this is a small enough app and I like to add a lot of customization and design specific elements. I also do not want to use the styled-components API.
I would like to only allow specific components as children. For example, let's say I have a Menu component, that should only contain MenuItem as children, like this:
<Menu>
<MenuItem />
<MenuItem />
</Menu>
So I would like Typescript to throw me an error in the IDE when I try to put another component as child. Something warning me that I should only use MenuItem as children. For example in this situation:
<Menu>
<div>My item</div>
<div>My item</div>
</Menu>
This thread is almost similar but does not include a TypeScript solution. I was wondering if the problem can be solved using TypeScript types and interfaces. In my imaginary world it would look like this, but of course the type checking is not working because the child component has an Element type:
type MenuItemType = typeof MenuItem;
interface IMenu {
children: MenuItemType[];
}
const MenuItem: React.FunctionComponent<IMenuItem> = ({ props }) => {
return (...)
}
const Menu: React.FunctionComponent<IMenu> = ({ props }) => {
return (
<nav>
{props.children}
</nav>
)
}
const App: React.FunctionComponent<IApp> = ({ props }) => {
return (
<Menu>
<MenuItem />
<MenuItem />
</Menu>
)
}
Is there a way to achieve this with Typescript? Like to extend the Element type with something related only to a specific component?
Or what would be a good approach for being sure that a child is an instance of a specific component? Without having to add condition that looks at the child component displayName.
To do that you need to extract props interface from children component (and preferably also parent) and use it this way:
interface ParentProps {
children: ReactElement<ChildrenProps> | Array<ReactElement<ChildrenProps>>;
}
so in your case it would look like this:
interface IMenu {
children: ReactElement<IMenuItem> | Array<ReactElement<IMenuItem>>;
}
const MenuItem: React.FunctionComponent<IMenuItem> = ({ props }) => {
return (...)
}
const Menu: React.FunctionComponent<IMenu> = ({ props }) => {
return (
<nav>
{props.children}
</nav>
)
}
Despite the answer above, you can't do this with children. You might do a runtime check in a dev build of your component, but you can't do this with TypeScript types — at least, not yet.
From TypeScript issue #18357:
Right now there's no way to specify what children representation is, except specifying ElementChildrenAttribute inside JSX namespace. It's heavily coupled with React representation for children which implies that children is a part of props. This makes impossible to enable type checking for children with implementations which store children separately, for instance https://github.com/dfilatov/vidom/wiki/Component-properties.
And note that it's referenced in #21699, where basically the possibly-breaking change around ReactElement may also make it possible to do this.
Right now, all you can do is that runtime check, or accept props (or arrays of props) and optionally a component function (in your case, you know it's MenuItem) and create the elements within your component.
There's also the question of whether you should do this. Why shouldn't I be able to write a component that returns a MenuItem rather than having to use MenuItem directly? :-)