JSPM

  • Created
  • Published
  • Downloads 343221
  • Score
    100M100P100Q172770F
  • License MIT

Like JSS but for optimized for TypeScript

Package Exports

  • tss-react
  • tss-react/@emotion/cache
  • tss-react/cache

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (tss-react) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

✨ Like JSS but optimized for TypeScript. Powered by emotion ✨

'tss-react' is intended to be a replacement for 'react-jss' and for @material-ui v4 makeStyle.
It's API is focused on providing maximum type safety and minimum verbosity.
This module is a tinny extension for @emotion/react.

$ yarn add tss-react @emotion/react

Quick start

./makeStyles.ts

import { createMakeStyles } from "tss-react";

function useTheme() {
    return {
        "primaryColor": "#32CD32",
    };
}

// material-ui users can pass in useTheme imported like: import { useTheme } from "@material-ui/core/styles";
// material-ui v5 users will also need to pass a custom emotion cache, read later.
export const { makeStyles } = createMakeStyles({ useTheme });

./MyComponent.tsx

import { makeStyles } from "./makeStyles";

const useStyles = makeStyles<{ color: "red" | "blue" }>()(
    (theme, { color }) => ({
        "root": {
            color,
            "&:hover": {
                "backgroundColor": theme.primaryColor,
            },
        },
    }),
);

export function MyComponent(props: Props) {
    const { className } = props;

    const [color, setColor] = useState<"red" | "blue">("red");

    const { classes, cx } = useStyles({ color });

    return <span className={cx(classes.root, className)}>hello world</span>;
}

With material-ui v4

Click to expand
import { render } from "react-dom";
import { StylesProvider } from "@material-ui/core/styles";

render(
    <StylesProvider injectFirst>
        <Root />
    </StylesProvider>,
    document.getElementById("root"),
);

If you need SSR You can find here a Next.js setup to use as reference.

With material-ui v5

Click to expand

Don't use <StyledEngineProvider injectFirst/> but do this instead:

import { render } from "react-dom";
import { CacheProvider } from "@emotion/react";
import createCache from "tss-react/@emotion/cache"; //Or "@emotion/cache"

export const muiCache = createCache({
    "key": "mui",
    "prepend": true,
});

render(
    <CacheProvider value={muiCache}>
        <Root />
    </CacheProvider>,
    document.getElementById("root"),
);

If you use SSR (server side rendering) you'll have to provide muiCache to the functions that enable SSR to work. See doc

WARNING: Keep @emotion/styled as a dependency of your project. Even if you never use it explicitly, it's a peer dependency of @material-ui/core v5.

WARNING for Storybook: As of writing this lines storybook still uses by default emotion 10.
Material-ui and TSS runs emotion 11 so there is some changes to be made to your .storybook/main.js to make it uses emotion 11.

Avoiding import { makeStyles } from "../../../makeStyles"

If you don't want to end up writing things like:

import { makeStyles } from "../../../../../../makeStyles";

You can put "baseUrl": "src" in your tsconfig.json and import things relative to your src/ directory.

Playground

Try it now:

API documentation

Exposed APIs

import {
    createMakeStyles, //<- Create an instance of makeStyle() for your theme.
    keyframe, //<- The function as defined in @emotion/react and @emotion/css
    GlobalStyles, //<- A component to define global styles.
    TssCacheProvider, //<- Provider to specify the emotion cache tss should use.
    useCssAndCx, //<- Access css and cx directly.
    //   (Usually you'll use useStyles returned by makeStyles or createMakeStyles for that purpose
    //    but if you have no theme in your project, it can come in handy.)
} from "tss-react";

makeStyles()

Your component style may depend on the props and state of the components:

const useStyles = makeStyles<{ color: string }>()((_theme, { color }) => ({
    "root": {
        "backgroundColor": color,
    },
}));

//...

const { classes } = useStyles({ "color": "grey" });

...Or it may not:

const useStyles = makeStyles()({
    //If you don't need neither the theme nor any state or
    //props to describe your component style you can pass-in
    //an object instead of a callback.
    "root": {
        "backgroundColor": "pink",
    },
});

//...

const { classes } = useStyles();

useStyles()

Beside the classes, useStyles also returns cx, css and your theme. css is the function as defined in @emotion/css cx is the function as defined in @emotion/css

const { classes, cx, css, theme } = useStyles(/*...*/);

In some components you may need cx, css or theme without defining custom classes.
For that purpose you can use the useStyles hook returned by createMakeStyles.

makeStyles.ts

import { createMakeStyles } from "tss-react";

function useTheme() {
    return {
        "primaryColor": "#32CD32",
    };
}

export const {
    makeStyles,
    useStyles, //<- This useStyles is like the useStyles you get when you
    //   call makeStyles but it doesn't return a classes object.
} = createMakeStyles({ useTheme });

./MyComponent.tsx

//Here we ca import useStyles directly instead of generating it from makeStyles.
import { useStyles } from "./makeStyles";

export function MyComponent(props: Props) {
    const { className } = props;

    const { cx, css, theme } = useStyles();

    return (
        <span className={cx(css({ "color": theme.primaryColor }), className)}>
            hello world
        </span>
    );
}

<GlobalStyles />

Sometimes you might want to insert global css. You can use the <GlobalStyles /> component to do this.

It's styles (with an s) prop should be of same type as the css() function argument.

import { GlobalStyles } from "tss-react";

function MyComponent() {
    return (
        <>
            <GlobalStyles
                styles={{
                    "body": {
                        "backgroundColor": "pink",
                    },
                    ".foo": {
                        "color": "cyan",
                    },
                }}
            />
            <h1 className="foo">This text will be cyan</h1>
        </>
    );
}

keyframe

// Reexport from @emotion/react
import { keyframe } from "tss-react";
import { makeStyles } from "./makeStyles";

export const useStyles = makeStyles()({
    "svg": {
        "& g": {
            "opacity": 0,
            "animation": `${keyframes`
            60%, 100% {
                opacity: 0;
            }
            0% {
                opacity: 0;
            }
            40% {
                opacity: 1;
            }
            `} 3.5s infinite ease-in-out`,
        },
    },
});

Cache

By default, tss-react uses an emotion cache that you can access with import { getTssDefaultEmotionCache } from "tss-react".
Now if you want tss-react to use a specific emotion cache you can provide it using import { TssCacheProvider } from "tss-react".

If you are using tss-react with mui v5, be aware that mui and tss can't share the same cache. On top of that the cache used by mui should have "prepend": true and the cache used by tss should have "prepend": false.

Composition and nested selectors ( $ syntax )

tss-react unlike jss-react doesn't support the $ syntax but there's type safe alternatives that achieve the same results.

Selecting children by class name

In JSS you can do:

{
  "parent": {
      "padding": 30,
      "&:hover $child": {
          "backgroundColor": "red"
      },
  },
  "child": {
      "backgroundColor": "blue"
  }
}
//...
<div className={classes.parent}>
    <div className={classes.children}>
        Background turns red when the mouse is hover the parent
    </div>
</div>

This is how you would achieve the same result with tss-react

const useStyles = makeStyles()((_theme, _params, createRef) => {
    const child = {
        "ref": createRef(),
        "background": "blue",
    } as const; //<- In many case 'as const' must be used so that it can be inferred as CSSObject

    return {
        "parent": {
            "padding": 30,
            [`&:hover .${child.ref}`]: {
                "background": "red",
            },
        },
        child,
    };
});

export function App() {
    const { classes } = useStyles();

    return (
        <div className={classes.parent}>
            <div className={classes.child}>
                Background turns red when mouse is hover the parent.
            </div>
        </div>
    );
}

Internal composition

When you want to reuse style within the same component.

import { makeStyles } from "./makeStyles";
import type { CSSObject } from "tss-react";

const useStyles = makeStyles<{ n: number; color: string }>()(
    (theme, { n, color }) => {
        const root: CSSObject = {
            "color": theme.primaryColor,
            "border": `${n}px solid black`,
        };

        return {
            root,
            "foo": {
                ...root,
                //Style specific to foo
                color,
            },
        };
    },
);

Export rules

MyComponent.tsx

import { makeStyles } from "./makeStyles";
// You can always define the Theme type as: "export type Theme = ReturnType<typeof useTheme>;"
import type { Theme } from "./makeStyles";
import type { CSSObject } from "tss-react";

//Can be used in another component
export const getRootStyle = (
    theme: Theme,
    params: { n: number },
): CSSObject => ({
    "color": theme.primaryColor,
    "border": `${params.n}px solid black`,
});

const useStyles = makeStyles<
    Parameters<typeof getRootStyle>[1] & { color: string }
>()((theme, { n, color }) => ({
    "root": getRootStyle(theme, { n }),
    // Other styles...
}));

Server Side Rendering (SSR)

There are some minimal configuration required to make tss-react work with SSR.

The following instructions are assuming you are using tss-react standalone or alongside @material-ui v5. You can find here a Next.js setup with @material-ui v4.

With Next.js

If you don't have a _document.tsx

Just create a file page/_document.tsx as follow:

import { createDocument } from "tss-react/nextJs";

const { Document } = createDocument();

/*
With mui v5 (or if you are using custom caches):

import { muiCache } from "...";

const { Document } = createDocument({ "caches": [ muiCache ] });

If you are providing custom caches to tss-react using <TssCacheProvider value={tssCache} >
you should pass it as well.

const { Document } = createDocument({ "caches": [ muiCache, tssCache ] });

Generally speaking all the emotion caches used in your app should be provided.
Just remember to first provide the caches used by mui then the caches used by tss. Example:

const { Document } = createDocument({ "caches": [ muiCache1, muiCache2, tssCache1, tssCache2 ] });
*/

export default Document;

You can find a working example here.

Or, if you have have a _document.tsx but you haven't overloaded getInitialProps

import Document from "next/document";
import type { DocumentContext } from "next/document";
import { createGetInitialProps } from "tss-react/nextJs";

const { getInitialProps } = createGetInitialProps();

/*
With mui v5 (or if you are using custom caches):

import { muiCache } from "...";

const { getInitialProps } = createGetInitialProps({ "caches": [ muiCache ] });

If you are providing custom caches to tss-react using <TssCacheProvider value={tssCache} >
you should pass it as well.

const { getInitialProps } = createGetInitialProps({ "caches": [ muiCache, tssCache ] });

Generally speaking all the emotion caches used in your app should be provided.
Just remember to first provide the caches used by mui then the caches used by tss. Example:

const { getInitialProps } = createGetInitialProps({ "caches": [ muiCache1, muiCache2, tssCache1, tssCache2 ] });
*/

export default class AppDocument extends Document {
    static async getInitialProps(ctx: DocumentContext) {
        return getInitialProps(ctx);
    }

    //...Rest of your class...
}

Or, if you have have a _document.tsx and an overloaded getInitialProps

import Document from "next/document";
import type { DocumentContext } from "next/document";
import { createPageHtmlToStyleTags } from "tss-react/nextJs";

const { pageHtmlToStyleTags } = createPageHtmlToStyleTags();
/*
With mui v5 (or if you are using custom caches):

import { muiCache } from "...";

const { pageHtmlToStyleTags } = createPageHtmlToStyleTags({ "caches": [ muiCache ] });

If you are providing custom caches to tss-react using <TssCacheProvider value={tssCache} >
you should pass it as well.

const { pageHtmlToStyleTags } = createPageHtmlToStyleTags({ "caches": [ muiCache, tssCache ] });

Generally speaking all the emotion caches used in your app should be provided.
Just remember to first provide the caches used by mui then the caches used by tss. Example:

const { pageHtmlToStyleTags } = createPageHtmlToStyleTags({ "caches": [ muiCache1, muiCache2, tssCache1, tssCache2 ] });
*/

export default class AppDocument extends Document {
    static async getInitialProps(ctx: DocumentContext) {
        const page = await ctx.renderPage();

        const initialProps = await Document.getInitialProps(ctx);

        return {
            ...initialProps,
            "styles": (
                <>
                    {initialProps.styles}
                    {pageHtmlToStyleTags({ "pageHtml": page.html })}
                </>
            ),
        };
    }

    //...Rest of your class...
}

With any other framework

yarn add @emotion/server
import { renderToString } from "react-dom/server";
import createEmotionServer from "@emotion/server/create-instance";

import { getTssDefaultEmotionCache } from "tss-react/cache";
import { createMakeStyles } from "tss-react";

const emotionServers = [
    getTssDefaultEmotionCache(), //If you use custom cache(s) provide it/them here instead of the default, see example below.
].map(createEmotionServer);

/*
With mui v5 (or if you are using custom caches):

import { muiCache } from "...";

const emotionServers = [
    muiCache,
    getTssDefaultEmotionCache()
].map(createEmotionServer);

If you are providing custom caches to tss-react using <TssCacheProvider value={tssCache} >
you should pass it as well.

const emotionServers = [
    muiCache,
    tssCache
].map(createEmotionServer);

Generally speaking all the emotion caches used in your app should be provided.
Just remember to first provide the caches used by mui then the caches used by tss. Example:

const emotionServers = [
    muiCache1,
    muiCache2,
    tssCache1,
    tssCache2
].map(createEmotionServer);
*/

const element = <App />;

const html = renderToString(element);

res.status(200).header("Content-Type", "text/html").send(`<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>My site</title>
    ${emotionServers
        .map(({ extractCriticalToChunks, constructStyleTagsFromChunks }) =>
            constructStyleTagsFromChunks(extractCriticalToChunks(html)),
        )
        .join("")}
</head>
<body>
    <div id="root">${html}</div>
    <script src="./bundle.js"></script>
</body>
</html>`);

Development

yarn
yarn build
#For automatically recompiling when file change
#npx tsc -w

# To start the Single Page Application test app (create react app)
yarn start_spa

# To start the Server Side Rendering app (next.js)
yarn start_ssr

# To start the Server Side Rendering app that test the mui v4 integration.
yarn start_muiV4

In SSR everything should work with JavaScript disabled

FAQ

Click to expand

Why this instead of the hook API of Material UI v4?

First of all because makeStyle is deprecated in @material-ui v5 but also because it has some major flaws. Let's consider this example:

import { makeStyles, createStyles } from "@material-ui/core/styles";

type Props = {
    color: "red" | "blue";
};

const useStyles = makeStyles(theme =>
    createStyles<"root" | "label", { color: "red" | "blue" }>({
        "root": {
            "backgroundColor": theme.palette.primary.main,
        },
        "label": ({ color }) => ({
            color,
        }),
    }),
);

function MyComponent(props: Props) {
    const classes = useStyles(props);

    return (
        <div className={classes.root}>
            <span className={classes.label}>Hello World</span>
        </div>
    );
}

Two pain points:

  • Because TypeScript doesn't support partial argument inference, we have to explicitly enumerate the classes name as an union type "root" | "label".
  • We shouldn't have to import createStyles to get correct typings.

Let's now compare with tss-react

import { makeStyles } from "./makeStyles";

type Props = {
    color: "red" | "blue";
};

const { useStyles } = makeStyles<{ color: "red" | "blue" }>()(
    (theme, { color }) => ({
        "root": {
            "backgroundColor": theme.palette.primary.main,
        },
        "label": { color },
    }),
);

function MyComponent(props: Props) {
    const { classes } = useStyles(props);

    return (
        <div className={classes.root}>
            <span className={classes.label}>Hello World</span>
        </div>
    );
}

Benefits:

  • Less verbose, same type safety.
  • You don't need to remember how things are supposed to be named, just let intellisense guide you.

Besides, the hook api of material-ui, have other problems:

  • One major bug: see issue
  • JSS has poor performances compared to emotion source

Why this instead of Styled component ?

See this issue