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

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:

Related

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

WithStyles injected props type

I am wrapping my class component with material UI withStyles to inject classes as a property.
export default withStyles(styles)(myComponent)
I have
const styles = ( (theme:Theme) => createStyles({className:CSS_PROPERTIES})
I am trying to declare an interface for my props as follows
interface MyComponentProps { classes : any }
What should I put instead of ANY ?
Based on this documentation piece, here is how you should do it:
import { withStyles, createStyles, Theme, WithStyles } from '#material-ui/core';
const styles = (theme:Theme) => createStyles({className:CSS_PROPERTIES})
interface MyComponentProps extends WithStyles<typeof styles> {
// you can type additional none-style related props of MyComponent here..
}
const MyComponent = ({ classes }: MyComponentProps) => {
// your component logic ....
};
export default withStyles(styles)(myComponent)

React JSS and TypeScript

I've been using React for a while, and now I want to switch to using React with TypeScript. However, I've grown used to JSS styles (via the react-jss package), and I can't understand how I'm supposed to use them with TypeScript. I also use the classnames package, to assign multiple class names conditionally, and I get TypeSCript errors for that.
Here is my React component template:
import React, { Component } from 'react';
import withStyles from 'react-jss';
import classNames from 'classnames';
const styles = theme => ({
});
class MyClass extends Component {
render() {
const { classes, className } = this.props;
return (
<div className={classNames({ [classes.root]: true, [className]: className})}>
</div>
);
}
};
export default withStyles(styles)(MyClass);
I'm just learning TypeScript, so I'm not sure I even understand the errors I get. How would I write something like the above in TypeScript?
UPDATE
Here is how I finally converted my template:
import React from 'react';
import withStyles, { WithStylesProps } from 'react-jss';
import classNames from 'classnames';
const styles = (theme: any) => ({
root: {
},
});
interface Props extends WithStylesProps<typeof styles> {
className?: string,
}
interface State {
}
class Header extends React.Component<Props, State> {
render() {
const { classes, className } = this.props;
return (
<div className={classNames({ [classes.root as string]: true, [className as string]: className})}>
</div>
);
}
};
export default withStyles(styles)(Header);
Things to keep in mind:
when defining the styles object, any member of classes that is referenced in the render method has to be defined. Without TypeScript, you could get away with "using" lots of classes and not defining them, like a placeholder; with TypeScript, they all have got to be there;
in a call to the classnames function, all the keys must be typed. If they come from a variable that could be null or undefined, you have to add as string, or to convert them to string otherwise. Other than this, the className property works the same as without TypeScript.
With TypeScript, you'll need to define your props as shown in here. It is also recommended to use function component if your React component only need render method
For your case, the code should look like this:
import React from 'react';
import withStyles, { WithStyles } from 'react-jss';
import classNames from 'classnames';
const styles = theme => ({
root: {
}
});
interface IMyClassProps extends WithStyles<typeof styles> {
className: string;
}
const MyClass: React.FunctionComponent<IMyClassProps> = (props) => {
const { classes, className } = props;
return (
<div className={classNames({ [classes.root]: true, [className]: className})}>
</div>
);
};
export default withStyles(styles)(MyClass);

Customize withStyles Material UI React styled components

I have a library of material-ui styled React components.
Styled components look like this:
import React from "react";
import withStyles from "#material-ui/core/styles/withStyles";
import headerStyle from "material-kit-pro-react/assets/jss/material-kit-pro-react/components/headerStyle.jsx";
class Header extends React.Component {
// ...
}
export default withStyles(headerStyle)(Header);
I want to customize the style of the component, but I want to keep the library untouched to avoid conflicts with future updates.
So I decided to create my own component:
import React from "react";
import withStyles from "#material-ui/core/styles/withStyles";
import Header from "material-kit-pro-react/components/Header/Header.jsx";
export default withStyles(theme => ({
primary: {
color: "#000"
}
}))(Header);
But with this approach the JSS engine creates two classes for the same component (Header-primary-25 WithStyles-Header--primary-12). That's not exactly a problem, but in my case there's a small conflict with a method in the component that expects only ONE class:
headerColorChange() {
const { classes, color, changeColorOnScroll } = this.props;
// ...
document.body
.getElementsByTagName("header")[0]
.classList.add(classes[color]) // classes[color] is a string with spaces (two classes)
;
// ...
}
Ok. It's not a big deal. I can modify that method and fix the problem.
But when I extend the class:
import React from "react";
import Header from "material-kit-pro-react/components/Header/Header.jsx";
export default class extends Header {
constructor(props) {
super(props);
this.headerColorChange = this.headerColorChange.bind(this);
}
headerColorChange() {
const { classes, color, changeColorOnScroll } = this.props
console.log(classes[color])
}
}
I get this error related to withStyles:
withStyles.js:125 Uncaught TypeError: Cannot read property '64a55d578f856d258dc345b094a2a2b3' of undefined
at ProxyComponent.WithStyles (withStyles.js:125)
at new _default (Header.jsx:11)
at new WithStyles(Header) (eval at ./node_modules/react-hot-loader/dist/react-hot-loader.development.js (http://localhost:8080/bundle.js:137520:54), <anonymous>:5:7)
And now I'm lost.
What's the best way to customize an already styled component?
How can I extend and override the constructor of a HoC (withStyles)?
Edit to provide a better and minimal example:
I have a component I can't modifiy because it's provided by a library:
./Component.jsx:
import React from "react";
import withStyles from "#material-ui/core/styles/withStyles";
class Component extends React.Component {
constructor(props) {
super(props);
this.headerColorChange = this.headerColorChange.bind(this);
}
componentDidMount() {
window.addEventListener("scroll", this.headerColorChange);
}
headerColorChange() {
const { classes } = this.props;
// THIS FAILS IF classes.primary IS NOT A VALID CLASS NAME
document.body.classList.add(classes.primary);
}
render() {
const { classes } = this.props;
return <div className={classes.primary}>Component</div>
}
}
export default withStyles(theme => ({
primary: {
color: "#000"
}
}))(Component);
I want to customize the primary color of the component. The straight way:
./CustomizedComponent.jsx:
import React from "react";
import withStyles from "#material-ui/core/styles/withStyles";
import Component from "./Component";
export default withStyles({
primary: {
color: "#00f"
}
})(Component)
Ok. The color of the has changed to #0ff. But then the component fails in document.body.classList.add(classes.primary) because classes.primary contains two class names concatenated (Component-primary-13 WithStyles-Component--primary-12). If I were allowed to modify the original component there would be no problem. But I can't, so I decided extend it and override the headerColorChange:
./CustomizedComponent.jsx:
import React from "react";
import withStyles from "#material-ui/core/styles/withStyles";
import Component from "./Component";
class MyComponent extends Component {
constructor(props) {
super(props)
this.headerColorChange = this.headerColorChange.bind(this);
}
headerColorChange() {
}
}
export default withStyles({
primary: {
color: "#00f"
}
})(MyComponent)
But then I get the error:
withStyles.js:125 Uncaught TypeError: Cannot read property '64a55d578f856d258dc345b094a2a2b3' of undefined
at ProxyComponent.WithStyles (withStyles.js:125)
at new MyComponent (Header.jsx:7)
at new WithStyles(Component) (eval at ./node_modules/react-hot-loader/dist/react-hot-loader.development.js (http://0.0.0.0:8080/bundle.js:133579:54), <anonymous>:5:7)
at constructClassInstance (react-dom.development.js:12484)
at updateClassComponent (react-dom.development.js:14255)
Questions are:
Is it possible to fully override the styles of the component so I get only one class name?
How can I extend a component returned by withStyles function as a higher order component?

styled-components strongly typed [theme] property

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

Resources