WithStyles injected props type - reactjs

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)

Related

Published styled-components UI library does not have access to extended theme types on the consumer side

I am creating UI library using styled-components. I am extending the DefaultTheme type to support custom themes that are needed for our use case. Both theme and theme definition is coming from a different internal package that I use. When working on the UI library components, it works correctly. The issue begins when we tried to use the theming on the consumer side.
The problem is that, it seems that types are not really extended to the consumer correctly, so without adding styled.d.ts file on the client side, it doesn't seem to understand the types of the theme. Theme get a type anyand it should be read asDefaultTheme type.
I wonder if there is any way to expose the extended types to consumer so they don't have to add this additional file on their side?
Is there anyone out there who had similar problem? Maybe you could share your findings?
Here is my setup:
Design System project:
// theme.ts
import tokens from 'my-external-package/variables.json';
import type { ThemeProps } from 'styled-components';
import type { MyThemeType } from 'my-external-package//theme';
const { light, dark } = tokens.color.theme;
export const lightTheme = {
color: light,
};
export const darkTheme = {
color: dark,
};
export const defaultTheme = lightTheme;
// styled.d.ts
import {} from 'styled-components';
import type { MyThemeType } from 'my-external-package//theme';
// extend theme
declare module 'styled-components' {
// eslint-disable-next-line #typescript-eslint/no-empty-interface
export interface DefaultTheme extends MyThemeType {}
}
// CustomThemeProvider.tsx
import React, { createContext, useState, ReactNode, useContext } from 'react';
import { ThemeProvider } from 'styled-components';
import { lightTheme, darkTheme } from './theme';
const themes = {
light: lightTheme,
dark: darkTheme,
};
type CustomThemeProviderProps = {
children: ReactNode;
defaultTheme?: keyof typeof themes;
};
const themeContext = createContext({ toggleTheme: () => {} });
const { Provider } = themeContext;
export const CustomThemeProvider = ({
children,
defaultTheme = 'light',
}: CustomThemeProviderProps) => {
const [currentTheme, setCurrentTheme] = useState(defaultTheme);
return (
<Provider
value={{
toggleTheme: () =>
setCurrentTheme((current) => current === 'light' ? 'dark' : 'light'),
}}
>
<ThemeProvider theme={themes[currentTheme]}>{children}</ThemeProvider>
</Provider>
);
};
// I also export hook over here so I can use it on the client side
export const useToggleTheme = () => {
const { toggleTheme } = useContext(themeContext);
return toggleTheme;
};
App consumer NextJs
//_app.tsx
import type { AppProps } from 'next/app';
import { CustomThemeProvider } from 'my-library-package/theming';
function MyApp({ Component, pageProps }: AppProps) {
return (
<CustomThemeProvider defaultTheme='light'>
<Component {...pageProps} />
</CustomThemeProvider>
);
}
export default MyApp;
// consumer_page.tsx
import type { NextPage } from 'next';
import { useCallback, useState } from 'react';
import styled from 'styled-components';
import tokens from 'my-external-package/variables.json';
import { useToggleTheme, Switch } from 'my-library-package';
const CustomComponent = styled.p`
color: ${({ theme }) => theme.color.feedback.success.foreground};
`;
const MyPage: NextPage = () => {
const toggleTheme = useToggleTheme();
return (
<>
<Switch onChange={toggleTheme}/>
<CustomComponent>This component have access to theme</CustomComponent>
</>
)
}
export default MyPage;
We are considering re-export utilities from styled-components with the right DefaultTheme and instruct consumers not to install styled-components
Instruct design-system consumers to create a styled.d.ts file to get the theme correctly populated.
Both of those seems rather painful. :(

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

React + Material-UI + Typescript: Inherit props from button to add different variants

I would like to extend the default Material-UI button to add more variants such as "square."
How can I define the prop interface to inherit/combine props.
Here's my code:
import React from "react";
import { Button as MuiButton } from "#material-ui/core";
import { ButtonProps as MuiButtonProps } from "#material-ui/core/Button";
interface ButtonProps extends MuiButtonProps {
variant?: "square";
}
const defaultProps = {};
const Button: React.FC<ButtonProps> = ({variant, children, ...props}) => {
return (
<MuiButton variant={variant} {...props}>
{props.children}
</MuiButton>;
)
};
Button.defaultProps = defaultProps;
export default Button;
Ideally, I would like to use this component like so:
<ExtendedButton href="/" variant="square">Click Me!</ExtendedButton>
The TYpeScript does not allows to override property when extending interfaces. So you should firstly exclude property from MuiButtonProps and then redefine it in ButtonProps.
import React from "react";
import { Button as MuiButton } from "#material-ui/core";
import { ButtonProps as MuiButtonProps } from "#material-ui/core/Button";
interface ButtonProps extends Pick<MuiButtonProps, Exclude<keyof MuiButtonProps, "variant">> {
variant?: "square" | MuiButtonProps["variant"];
}
Property exclusion with Pick and Exclude is for TypeScript 3.5 and below to 2.8. You may see another options to exclude properties depending on TypeScript version you use.
And as you wish to extend current type of existing property variant, you can use index access operator to get type of original variant property to union it with additional "square" type.

Using material design for React with typescript

I am looking for a pattern to use react material design with typescript but I am having a few issues.
export defaults seems to be an anti pattern, and the options in my tsconfig will not allow me to do that. I am wondering is their a better way to do this without disabling it through the tsconfig.
what about a component with state which uses a class instead of functions, how would I apply withstyles to a class.
Could anybody please help me to apply the withstyles to my typical stateful component pattern without introducing anti patterns.
Typical Statefull component pattern:
import * as React from "react";
export namespace Header {
export interface Props {
}
}
export class Header extends React.Component<Header.Props> {
constructor(props: Header.Props, context?: any) {
super(props, context);
}
public render(): JSX.Element {
return (
<div> HEADER HTML </div>
);
}
}
MATERIAL UI RECOMMENDED APPROACH:
import s from './Styleguide.scss';
function Styleguide() {
...
}
function LinksComponent() {
...
}
function ButtonsComponent() {
...
}
const Links = withStyles(s)(LinksComponent);
const Buttons = withStyles(s)(ButtonsComponent);
export {
Links,
Buttons
};
export default withStyles(s)(Styleguide);
just export your styled component as a named export?
import * as React from "react";
import s from './Styleguide.scss';
export namespace Header {
export interface Props {
}
}
class BareHeader extends React.Component<Header.Props> {
constructor(props: Header.Props, context?: any) {
super(props, context);
}
public render(): JSX.Element {
return (
<div> HEADER HTML </div>
);
}
}
const Header = withStyles(s)(BareHeader);
export { Header };

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?

Resources