styled-components strongly typed [theme] property - reactjs

I am using styled-components TypeScript and ThemeProvider for my components, and I had a couple of questions:
First of all, my components are created using map, so I used to assign a key to each one, now I have put ThemeProvider to be the top parent component, and hence I need to set the key on that. I was just wondering does it hurt to do this? Or should I find a way to create a single ThemeProvider?
Since I'm using TypeScript, it would be very nice if I could somehow make my props.theme property be strongly typed. Right now when I hover over props.theme, I see that the type is any. It would VERY nice if I could somehow define the type for the theme property while not changing the inferred type for props
The problem I have right now is that when I define a custom interface for the props I'm using in a styled component, I loose the default properties inferred by the component. For example, if I want to have something like this:
interface ComponentProps {
status: string;
}
Then, I create a component like this:
const MyComp = styled.div`
background-color: ${(props: ComponentProps) => props.theme...};
`
Then, TypeScript will complain that theme doesn't exist on ComponentProps, but if I don't define the type for the props, then when I want to create my custom component:
<MyComp status="hello" />
Now, the TypeScript is complaining that property status doesn't apply to MyComp
I would appreciate any help

You can create a container, and pass any property you like. Like this:
in styledWithProps.ts file:
import { ThemedStyledFunction } from 'styled-components';
export const styledWithProps = <U>() =>
<P, T, O>(
fn: ThemedStyledFunction<P, T, O>
): ThemedStyledFunction<P & U, T, O & U> => fn;
when using it:
import styled, { css } from "styled-components";
import { styledWithProps } from "./styledWithProps.ts";
const MyComp = styledWithProps<any>()(styled.div) `
${props => props.theme==="dark" && css`
background-color: black;
`}
`;
const app = props => <MyComp theme="dark">{props.children}</MyComp>
For more details, take a look this thread: https://github.com/styled-components/styled-components/issues/630

Related

className attribute won't take CSSProperties type - React, Material-UI, with TypeScript 4.1

I am new to Material-UI. We are using withStyles to wrap the component.
I followed the docs here to create the classes object with the correct types.
const classes = createStyles({
mainDiv: {
display: "flex",
flexDirection: "column",
justifyContent: "center"
...
Then I pass those down to my component in the className attribute, like this.
render() {
const { classes } = this.props;
return <div id="mainDiv" className={classes.mainDiv}>
...
I get a TypeScript error under className which says:
Type 'CSSProperties | CreateCSSProperties<{}> | PropsFunc<{}, CreateCSSProperties<{}>>' is not assignable to type 'string | undefined'.
This error makes sense to me because in normal JSX, the className attribute should receive a string. I find it very odd that Material-UI passes it an object, but I guess that's its special thing. So how do I let TypeScript know that this normal old div's className attribute should be the same special thing?
The createStyles function doesn't create class names. It just returns the styles that you gave it. Its only purpose is to check the validity of the styles by forcing them to conform to the CSSProperties type. Per the docs:
TypeScript widens the return types of function expressions. Because of this, using the createStyles helper function to construct your style rules object is recommended. createStyles is just the identity function; it doesn't "do anything" at runtime, just helps guide type inference at compile time.
You would need to apply your styles to the style property rather than the className property:
return <div id="mainDiv" style={classes.mainDiv}/>
Or use a different function which creates string class names, such as makeStyles:
// creates a hook which maps props to class names
const useStyles = makeStyles(classes);
const MyComponent = (props: {}) => {
// call the created hook with the component props
const {mainDiv} = useStyles(props);
// can use as className property
return <div id="mainDiv" className={mainDiv} />;
};
When using a class component and the withStyles higher-order component. You need to extend the props type for your component using the WithStyles type utility. Like so:
import { WithStyles, createStyles } from '#material-ui/core';
const styles = (theme: Theme) => createStyles({
mainDiv: { ... },
button: { ... },
...
});
interface Props extends WithStyles<typeof styles> {
foo: number;
bar: boolean;
}
//or
type Props = WithStyles<typeof styles> & {
foo: number,
bar: boolean
}

React, styled-components and TypeScript: How to wrap a styled component in a functional component

I'm trying to create a wrapper for my styled component but I'm having trouble getting the types right.
Let's say I have a styled component like this:
const Button = styled.button<ButtonProps>`
background-color: ${(props) => props.color};
`;
Now I want to create a wrapper component that contains this styled button, eg. like this:
const WrappedButton: React.FunctionComponent<ButtonProps> = ({ children, ...rest }) => (
<div>
<Button {...rest}>{children}</Button>
</div>
);
Now this all works fine, but what I actually want is the WrappedButton component to accept all props that the Button component would accept and pass them along to the wrapped Button component.
So for example I want this to compile, as type is a valid prop of a HTML button element (and therefore also a valid prop of the Button component, but not when the Button component is wrapped):
// TypeScript will throw an error because "type" is not a valid prop of WrappedButton.
const MyComponent = () => <WrappedButton type="submit">Test</WrappedButton>
I know I can make "type" a prop of the WrappedComponent, but that's not the point, I want the WrappedComponent to accept all props that a normal HTML button would accept.
EDIT: Also I need all styled-component specific props on the wrapped component, such as the as prop of styled-components. Here's an updated version of the code sandbox: https://codesandbox.io/s/react-typescript-styled-components-forked-3o20j?file=/src/index.tsx
I have tried so many things but TypeScript is always complaining. I have also searched the docs and the internet but have not found anything.
I believe You are asking about React.ButtonHTMLAttributes<HTMLButtonElement>.
import React from 'react'
import styled from 'styled-components'
const Button = styled.button<ButtonProps>`
background-color: ${(props) => props.color};
`;
type ButtonProps = {
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
const WrappedButton: React.FunctionComponent<ButtonProps> = ({ children, ...rest }) => (
<div>
<Button {...rest}>{children}</Button>
</div>
);
If you want to use raw html <button>, you mignt be interested in: JSX.IntrinsicElements["button"];
Check this answer
UPDATE
The quick and dirty solution would be :
type ButtonProps = {
} & React.ButtonHTMLAttributes<HTMLButtonElement> & { as?: string | React.ComponentType<any> };
But it is not generic.
There is StyledComponentPropsWithAs type in SC typings, but it is not exported (
If you're trying to suppress native attributes you could do something like this:
interface Props {
type?: 'error' | 'success'
}
type OmitNativeAttrs = Omit<React.HTMLAttributes<HTMLButtonElement>, keyof Props>
export type ButtonProps = Props & OmitNativeAttrs

How do I use a StyledComponent where React.Component is expected?

I'm trying to create a typescript React app that and have run into an issue when using styled-components. Below is a rough idea of what I'm attempting:
import React from 'react';
import styled from 'styled-components';
export type MyProps = {
wrapper?: React.Component,
};
const DefaultWrapper = styled.div`
background: blue;
`;
const MyComponent = ({wrapper: Wrapper = DefaultWrapper}: MyProps) => {
return <Wrapper className='my-wrapper'>
Some child content
</Wrapper>;
}
export default MyComponent;
My issue comes when I try to render MyComponent within another component as it throws an error saying JSX element type 'Wrapper' does not have any construct or call signatures.
I'd like some way that I could use a styled component as either the default value or as a valid value for the wrapper prop in such a way as to not expose that I'm using styled components internally. Any help would be appreciated.
Problem
React.Component is a React class element (not a type), instead, use the type React.ComponentType<any>. If you're working with component type constraints and expect wrapper to be certain type of component, then swap out the React.ComponentType<any> with the necessary constraint(s) -- like React.ComponentClass<any, any> for class-based components or React.FC<any> for functional components.
Solution
import * as React from "react";
import styled from "styled-components";
export type MyProps = {
wrapper?: React.ComponentType<any>;
};
const DefaultWrapper = styled.div`
background: blue;
`;
const MyComponent = ({ wrapper: Wrapper = DefaultWrapper }: MyProps) => {
return <Wrapper className="my-wrapper">Some child content</Wrapper>;
};
export default MyComponent;
Working repo
Ignore the 'React' was used before it was defined. eslint warning, it's a codesandbox issue -- works fine locally:

No overload matches this call when usign styled-components

There are several questions about this error on SO but none from anybody in a similar situation and none of the solutions I have found work so I'm going to post a specific question.
When using a Styled component inside another component and passing props to it, do I have to create a new type to pass the props through to the styled component or is there some way of using the existing styled component types?
Here is my code. It seems to be the as="" attribute which is giving the error above. I assume this is something to do with the fact that my component only takes the prop so it can pass it to the Styled Component as it is a feator of Styled Components.
import React from 'react'
import styled from 'styled-components'
type Props = {
additionalClass?: string,
as?: string
}
const Component: React.FC<Props> = (props) => {
return (
<StyledComponent as={props.as} className={props.additionalClass}>
{props.children}
</StyledComponent>
)
}
export default Component
const StyledComponent = styled.div`
...styles go here
`
You just need to validate the prop when you are creating the styled.div
Example
const Menu= styled.div<{ menuOpen: boolean }>`
${({ menuOpen }) => {
if (menuOpen) {
return 'background-color: white';
}
return 'background-color: black';
}}
`;
export const Component: React.FC = () => {
const [menuOpen, setMenuOpen] = useState(false);
return(
<Menu menuOpen={menuOpen}>
whatever content
</Menu>
)
}
The problem was that I was trying to pass the "as" prop to styled components and I thought it needed to be a string so I did this...
type Props = {
className?: string,
as?: string
}
The problem is that once you pass it to styled-components it expects it to be the name of an element. I found the answer on Github and it can be one of two different things. This works...
type Props = {
className?: string,
as?: React.ElementType | keyof JSX.IntrinsicElements
}
type Props = {
additionalClass?: string,
[key: string]: any
}

How to create dynamic styled component?

What I'm trying to achieve is something like this:
import styled from 'react-emotion'
const StyledComponent = styled(({tag}) => tag)`
// some css styles
`
And use it like:
<StyledComponent tag=div"/>
<StyledComponent tag="p"/>
<StyledComponent tag="ul"/>
// etc
My expectation was that it should generate HTML as follows:
<div class="some-class"></div>
<p class="some-class"></p>
<ul class="some-class"></ul>
Actual output:
div
p
ul
My question is, can this be achieved or am I missing anything?
Seems like I have figured out a solution to my problem. Sharing my answer for those who may run into the same problem.
I changed StyledComponent declaration to following:
import styled from 'react-emotion'
const StyledComponent = styled(({tag, children, ...props}) => React.createElement(tag, props, children))`
// some css styles
`
This is working as expected.
If anyone has the better answers please do post. Thank you
You are using react-emotion in wrong way, please try this.
const StyledComponent = ({ tag, children, ...props }) => {
const Container = styled(tag)`
background-colo: red;
`;
return <Container {...props}>{children}</Container>;
};
Demo: https://codesandbox.io/s/lr4xxp3757
What worked form me was using the "as" polymorphic prop (https://styled-components.com/docs/api#as-polymorphic-prop):
const StyledComponent = styled.div``
type ComponentProps = {
tag?: AnyStyledComponent
children: React.ReactNode
}
const Component = ({ tag, children }: ComponentProps) => (
<StyledComponent as={tag}>{children}</StyledComponent>
)
Declaring the styled component outside of the function component will spare us from console warnings such as this one: https://github.com/styled-components/styled-components/issues/3117

Resources