Categories
Projects

Merging Material UI and styled-component themes and their types with TypeScript

Recently I ran into a problem with Material UI, styled-components, and TypeScript that I had a lot of trouble solving. Now that I have a working solution, I think its right to share what I did in case someone else runs into this problem.

The Scenario:

This is probably an uncommon scenario but probably not unique:

  1. The app is built mainly with React and Next.js, with most of the UI relying on Material UI (v4).
  2. We are using styled-components as a CSS-in-JS solution. I like this because it lets me use “real” css which I strongly prefer to the emotion style/style object syntax. Also, many of the components were built with styled-components before we adopted material UI, so it would be some effort to refactor that out.
  3. Since we introduced Material UI, we have been passing a single, unified theme object to both the styled-components theme provider and the material ui theme provider, which both wrap the entire app. This is done by passing the output of createTheme from material ui to both as the theme prop. This works quite nicely because it lets us use stuff like theme.breakpoints.down('md')
    in our styled-components CSS.
  4. We have just introduced TypeScript. This is where things get challenging.

The Problem

Once we started converting files to TypeScript (and forgive me if anything comes across as ignorant here because I’m pretty new to TypeScript and strongly typed languages in general), we had to create a declaration for the DefaultTheme in styled-components. Since we are using the theme provided by createTheme from Material UI in our styled-components app, the situation is pretty messy. The type Theme from material-ui is not compatible with the type DefaultTheme from styled components.

The Solution

Ultimately, here’s how I fixed it.

1. Declare the styled-components DefaultTheme with my own types, filling in the missing types from material ui ThemeOptions to fill in missing values

// types/styled-components.d.ts

declare module 'styled-components' {
    export interface DefaultTheme {
        palette: palette;
        overrides: overrides;
        componentVariables: componentVariables;
        breakpoints: breakpoints;
        typography: typography;
        spacing: ThemeOptions['spacing']; // this comes from Mui
        props: ThemeOptions['props']; // this comes from Mui
        mobileToDesktopBreakpoint: number;
    }
}

(each of the properties in the DefaultTheme interface references a uniquely defined type but I left them out of the example for brevity)

2. Create two themes, one for MUI and one for SC from the same ThemeOptions root object

For the styled-components theme I cherry-picked properties from the Theme object from createTheme. It looks like this:

// theme.ts

const themeOptions = {
/** 
    this is where all the definitions for our theme go, such as custom palette colors, componentOverrides, etc 
**/
}

const muiThemeOptions: ThemeOptions = {
  ...(themeOptions as ThemeOptions),
  spacing: 8,
};

const muiTheme = createTheme(muiThemeOptions);

const scTheme: DefaultTheme = {
  ...(themeOptions as DefaultTheme), // spread the props from themeOptions
  breakpoints: muiTheme.breakpoints, // add the properties from createTheme
  spacing: muiTheme.spacing, 
};

export {muiTheme, scTheme};
// _app.tsx

import {ThemeProvider as StyledComponentsThemeProvider} from 'styled-components';
import {ThemeProvider as MuiThemeProvider} from '@material-ui/core';
import {muiTheme, scTheme} from 'styles/theme/theme';

function App({Component}) {
    return (
    <MuiThemeProvider theme={muiTheme}>                   
        <StyledComponentsThemeProvider theme={scTheme}>
            <Component {...pageProps} />
        </StyledComponentsThemeProvider>
    </MuiThemeProvider> 
    );
}

3. Now we can use the mui theme values in our styled components

// SomeComponent.tsx

const StyledElement = styled.div`
    ${({theme}) => theme.breakpoints.down('md')} {
        left: -200px;
    }
`;

This solution came after some frustration around TypeScript telling me that I can’t use the Theme type as the DefaultTheme type that was expected to be passed to the styled-components theme provider. Since we had already merged themes before we were using TypeScript, in which case this was not an issue at all, my options were to figure out a way to merge the type definitions or refactor out all our uses of the MuiTheme properties in our styled-components. Our team finds it pretty useful to be able to use functions like theme.breakpoints.down() and theme.spacing() in our styled components, so this is definitely the solution we prefer for now.

Did this help you? Am I missing something or making a mistake in my understanding of types and TypeScript? Any other feedback? Let me know in the comments.

Leave a Reply

Your email address will not be published.