Typechecking React library with Styled Components - reactjs

I'm currently working in a design system library to test some things.
Basically, the library is a Styled Component wrapper in order to create themes.
I built this library with Typescript, React, Styled Components and Rollup as bundler.
The problem is when the user use this library, the vscode doesn't helps with the typechecking for the theme declarations.
This is the index.js from the library. Basically is a HOC in which we have a context and a ThemeProvider HOC from Styled components.
import { createContext, useState } from "react";
import "styled-components";
import {
DefaultTheme,
ThemeProvider as SCThemeProvider,
} from "styled-components";
import { themes } from "./design-system/colorTheme";
import React from "react";
interface ThemeContextAPI {
toggleTheme: () => void;
currentTheme: DefaultTheme;
}
const ThemeContext = createContext<ThemeContextAPI | null>(null);
const ThemeProvider: React.FC<{ children?: React.ReactNode }> = ({
children,
}) => {
const [currentTheme, setCurrentTheme] = useState<DefaultTheme>(themes.light);
const toggleTheme = () => {
setCurrentTheme((prev) =>
prev === themes.light ? themes.dark : themes.light
);
};
const values: ThemeContextAPI = {
toggleTheme,
currentTheme,
};
return (
<ThemeContext.Provider value={values}>
<SCThemeProvider theme={currentTheme}>{children}</SCThemeProvider>
</ThemeContext.Provider>
);
};
export default ThemeProvider;
export { ThemeContext };
As we see, we have a SCThemeProvider that is a ThemeProvider component from Styled component. This brings to the user, use all the theme variables inside all the styled components. The theme that this component provides is extended as the SC documentation says, so DefaultTheme is declared like this:
export interface Theme {
primary?: Color;
primaryContainer?: Color;
secondary?: Color;
secondaryContainer?: Color;
tertiary?: Color;
tertiaryContainer?: Color;
cuaternary?: Color;
cuaternaryContainer?: Color;
// -------------
surface?: Color;
surfaceVariant?: Color;
background?: Color;
error?: Color;
errorContainer?: Color;
// -------------
onPrimary?: Color;
onPrimaryContainer?: Color;
onSecondary?: Color;
onSecondaryContainer?: Color;
onTertiary?: Color;
onTertiaryContainer?: Color;
onCuaternary?: Color;
onCuaternaryContainer?: Color;
// --------------
onSurface?: Color;
onSurfaceVariant?: Color;
onError?: Color;
onErrorContainer?: Color;
onBackground?: Color;
outline?: Color;
// --------------
inverseSurface?: Color;
inverseOnSurface?: Color;
inversePrimary?: Color;
colorPalette: ColorPalette<ColorSet>;
surfaceTones: SurfaceTones;
}
declare module "styled-components" {
export interface DefaultTheme extends Theme {}
}
The library is consumed like this, (The example is written in next.js):
import { GlobalStyle, ThemeProvider } from "rakes-design-sys";
function MyApp({ Component, pageProps }) {
return (
<>
<ThemeProvider>
<GlobalStyle />
<Component {...pageProps} />
</ThemeProvider>
</>
);
}
export default MyApp;
But when I use the theme inside a styled component, the vscode doesn´t shows the types of the theme. I´ve already configured TS config to create declarations files but still doesn't work. I just put this in tsconfig and rollupconfig
"declaration": true,
"declarationDir": "types",
"emitDeclarationOnly": true
And I use the rollup-plugin-dts like this in my rollup configuration
{
input: "dist/types/index.d.ts",
output: [{ file: "dist/index.d.ts", format: "es" }],
plugins: [dts()],
},
This generates a folder inside dist called types in which is contained all my files in the library but with the .d.ts extensions and creates another index.d.ts inside dist.
By this way typechecking works when I use the context like:
import { ThemeContext } from "rakes-design-sys";
import { useContext, useEffect } from "react";
import { OHeader } from "../styles/organisms/OHeader.styles";
const App = () => {
const { toggleTheme, currentTheme } = useContext(ThemeContext);
console.log(currentTheme.primaryContainer);
return <OHeader onClick={() => toggleTheme()}>HelloWorld</OHeader>;
};
export default App;
In the console.log(currentTheme.something) the typechecking is working but this doesn´t work inside a styledComponent like this
import styled from "styled-components";
export const OHeader = styled.div`
background-color: ${(props) => props.theme.primary};
`;
It's already working, but when I write props.theme.something, it doesn't predict or typechecking the values inside the current theme that the styled components is providing.

The provider context for styled-components acts as a singleton. So, when you import from styled-components in your library and in your consumer app, the theme context doesn't match.
To fix make styled-components a peer dependency in your library.

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. :(

not able to pass theme to "#mui/system" styled()

I am attempting to pass a theme by doing:
declare module "#mui/styles/defaultTheme" {
// eslint-disable-next-line #typescript-eslint/no-empty-interface
interface DefaultTheme extends Theme {}
}
ReactDOM.render(
<StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}>
<Main />
</ThemeProvider>
</StyledEngineProvider>,
document.getElementById("root")
);
which I them attempt to read by doing:
import { styled } from "#mui/system";
type StyledTabProps = {
isActive: boolean;
};
const StyledTab = styled("div", {
shouldForwardProp: (prop) => prop !== "isActive"
})<StyledTabProps>(({ theme, isActive }) => {
console.log("theme", theme);
return {
color: isActive ? "red" : "blue"
};
});
the theme which I attempt to pass is different from the one that ends up getting console.logged (is missing properties in palette object)
code sandbox of the issue can be found here:
https://codesandbox.io/s/wandering-https-ubhqs?file=/src/Main.tsx
You just need to use ThemeProvider from #mui/material/styles, not from #mui/styles. Then it would work.
Please refer to this
// index.tsx
...
import { Theme, ThemeProvider, StyledEngineProvider } from "#mui/material/styles";
...
And also MUI v5 theme structure is a bit different not exactly the same to the v4.
You could not use adaptV4Theme, just update the theme structure, but please define some custom types for it.
(The codesandbox says adaptV4Theme is deprecated somehow.)
For example, you used overrides option for the theme object, but it is removed so you need to use components instead of it. https://mui.com/guides/migration-v4/#theme-structure
...
// styles/theme.ts
export default createTheme(
{
...
components: {
MuiInputBase: {
root: {
...
}
...

How do I bundle MUI theme with rollup

I've been working on pulling our existing react FE components out of our main repo, and into a separate repo that I am bundling with rollup. Our old code was using makeStyles and I have been switching that over to styled-components but still keeping the previous MUI theme. I've setup storybook and am wrapping that in styled components theme provider, in order to access the theme inside the styled components.
The structure looks like
components
\src
index.ts(imports and exports components)
\theme(MUI theme)
\components
\buttons
button.tsx(react button code)
index.ts(imports and exports button)
\lib(rollup spits this out)
Finally, to the question. After I bundle everything with rollup, I do an NPM install, and import it into a different project. The problem is, I'm not getting the proper theming in the imported components. Here is a somewhat simplified version of my button.
import React from "react";
import { Button as MaterialButton, ButtonProps } from "#material-ui/core";
import styled from "styled-components";
export interface MyButtonProps extends ButtonProps {
error?: boolean;
target?: string;
}
const StyledButton = styled(MaterialButton)`
&.error {
background: ${(props) => props.theme.palette.error.main};
color: #fff;
&:hover {
background: ${(props) => props.theme.palette.error.main};
}
}
`;
const Button = ({
error,
className,
...rest}: MyButtonProps) => {
className += error ? " error" : "";
return (
<StyledButton
{...rest}
className={className}
>
{children}
</StyledButton>
);
};
export default Button;
So, if I put error attribute on the button, I do get the correct color from my theme. However, if I put color="primary" I do not get the correct color. I also don't have any of my base styles from the theme.
I haven't been able to figure out how to get this theme into the components I'm bundling with rollup. Finally, here is my rollup config.
import peerDepsExternal from "rollup-plugin-peer-deps-external";
import resolve from "#rollup/plugin-node-resolve";
import commonjs from "#rollup/plugin-commonjs";
import typescript from "rollup-plugin-typescript2";
import postcss from "rollup-plugin-postcss";
import svg from "rollup-plugin-svg-import";
const packageJson = require("./package.json");
export default {
input: "src/index.ts",
output: [
{
file: packageJson.main,
format: "cjs",
sourcemap: true,
},
{
file: packageJson.module,
format: "esm",
sourcemap: true,
},
],
plugins: [
peerDepsExternal(),
resolve(),
commonjs(),
svg(),
typescript({ useTsconfigDeclarationDir: true }),
postcss({
extensions: [".css"],
}),
],
};
For anyone who has stumbled upon this and is curious about the answer. Here is what I ended up doing. Hopefully this is the correct solution. One thing that is a bummer, I end up with MUI ThemeProvider giving me it's generated class names. This means in my styled components, I'll occasionally need to do this to target things [class^="MuiSvgIcon-root"], but maybe i can access the class through props, still messing with it. Anyway, here is the button.
import React from "react";
import { Button as MaterialButton, ButtonProps } from "#material-ui/core";
import styled from "styled-components";
import { ThemeProvider } from "#material-ui/core/styles";
import theme from "../../../theme";
export interface MyButtonProps extends ButtonProps {
error?: boolean;
target?: string;
}
const StyledButton = styled(MaterialButton)`
&.error {
background: ${theme.palette.error.main};
color: #fff;
&:hover {
background: ${theme.palette.error.main};
}
}
`;
const Button = ({
error,
size,
variant,
endIcon,
children,
className,
...rest
}: MyButtonProps) => {
if (className && error) {
className += " error";
} else if (!className && error) {
className = "error";
}
return (
<ThemeProvider theme={theme}>
<StyledButton
{...rest}
variant={variant}
size={size}
endIcon={endIcon}
className={className}
>
{children}
</StyledButton>
</ThemeProvider>
);
};
export default Button;
I guess each component will need to be wrapped.

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

material-ui-next - Dynamically set palette color

I am using "material-ui": "^1.0.0-beta.33" for my project.
What I want to do is set primary palette color dynamically inside a react component (color will be fetched from some api).
Basically I want to override below:
const theme = createMuiTheme({
palette: {
primary: "some color from api"
},
})
Is there a way to set this in componentDidMount function of any component?
Reference: https://material-ui-next.com/
I created a component that uses MuiThemeProvider and wrap my entire app around that component. Below is the structure of the component.
import React, {Component} from "react";
import {connect} from "react-redux";
import {withStyles} from 'material-ui/styles';
import * as colors from 'material-ui/colors';
import { MuiThemeProvider, createMuiTheme } from 'material-ui/styles';
import { withRouter } from 'react-router-dom';
export class ThemeWrapperComponent extends Component {
constructor(props){
super(props);
}
render(){
return (
<MuiThemeProvider theme={createMuiTheme(
{
palette: {
primary: { main: **colorFromApi** },
}
)}>
<div>
{ this.props.children }
</div>
</MuiThemeProvider>
)
}
}
export const ThemeWrapper = withRouter(connect(mapStateToProps)(ThemeWrapperComponent));
Below is how I wrapped my app around this component:
<ThemeWrapper>
<div>
<Routes/>
</div>
</ThemeWrapper>
Now, whatever colour you are sending from the api gets applied to the whole theme. More customisation can be done based on requirement.
I'm doing exactly this. Even got it working with WebMIDI using MIDI controller sliders and knobs just for fun.
The basic strategy is to use createMuiTheme and ThemeProvider and to store the theme in your application store (context, state, redux), etc.
class ThemeManager extends React.Component {
getThemeJson = () => this.props.context.themeJson || defaultThemeJson
componentDidMount () {
const themeJson = this.getThemeJson()
const theme = createMuiTheme(themeJson)
this.props.setContext({ theme, themeJson })
}
render () {
const { children, context } = this.props
const theme = context.theme
return theme
? <ThemeProvider theme={theme}>{children}</ThemeProvider>
: children
}
}
https://github.com/platform9/pf9-ui-plugin/blob/master/src/app/ThemeManager.js
and then you simply update your application's state.
handleImport = themeStr => {
const themeJson = JSON.parse(themeStr)
const theme = createMuiTheme(themeJson)
this.props.setContext({ theme, themeJson })
}
https://github.com/platform9/pf9-ui-plugin/blob/master/src/app/plugins/theme/components/ImportExportPanel.js#L17

Resources