JSPM

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

Like JSS but for optimized for TypeScript

Package Exports

  • tss-react

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

Quick start

./MyComponent.tsx

import { makeStyles } from "./styleEngine";

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

function MyComponent(props: Props){

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

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

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

}

./styleEngine.ts

import { createMakeStyles } from "tss-react";

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

// material-ui users can pass in useTheme imported like: import { useTheme } from "@material-ui/core/styles";
export const { makeStyles, useCssAndCx } = createMakeStyles({ useTheme });

Material-UI users only, don't forget to enable injectFirst

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

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

Try it now:

API documentation

import { makeStyles, useCssAndCx } from "./styleEngine";

const { useStyles } = makeStyles<{ color: "red" | "blue" }>()(
    //NOTE: This doesn't have to be a function, it can be just an object.
    (theme, { color }) => ({
        "fooBar": {
            "width": 100,
            "height": 100,
        },
    }),
);

export function MyComponent(props: { className?: string }) {
    //css and cx are the functions as defined in @emotion/css: https://emotion.sh
    //theme is the object returned by your useTheme()
    const { classes, css, cx, theme } = useStyles({ "color": "red" });

    //You can also access css and cx with useCssAndCx()
    //const { css, cx }= useCssAndCx();

    return (
        <div className={cx(classes.fooBar, css({ "backgroundColor": theme.limeGreen }), className)} />
    );
}
import { createMakeStyles } from "tss-react";

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

export const { makeStyles, useCssAndCx } = createMakeStyles({ useTheme });

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 "./styleEngine";

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

Canonical usage example

Consider this example to understand how css, cx and makeStyles are supposed to be used together:

MyButton.tsx

import { makeStyles } from "./styleEngine";

export type Props = {
    text: string;
    onClick(): void;
    isDangerous: boolean;
    className?: string;
};

const { useStyles } = makeStyles<Pick<Props, "isDangerous">>()((theme, { isDangerous }) => ({
    "root": {
        "backgroundColor": isDangerous ? "red" : "grey",
    },
}));

export function MyButton(props: Props) {
    const { className, onClick, text } = props;

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

    return (
        <button
            //You want to apply the styles in this order
            //because the parent should be able ( with
            //the className prop) to overwrite the internal
            //styles ( classesNames.root )
            className={cx(classes.root, className)}
            onClick={onClick}
        >
            {text}
        </button>
    );
}

App.tsx

import { useCssAndCx } from "./styleEngine";

function App() {
    const { css } = useCssAndCx();

    return (
        <MyButton
            //The css function return a className, it let you
            //apply style directly on in the structure without
            //having to use createUseClassNames
            className={css({ "margin": 40 })}
            text="click me!"
            isDangerous={false}
            onClick={() => console.log("click!")}
        />
    );
}

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

In SSR everything should work with JavaScript disabled

Server Side Rendering (SSR)

In order to get server side rendering to work you have to use a provider.

shared/styleEngine.ts

import { createMakeStyles } from "tss-react";
import { useTheme } from "@material-ui/core/styles";

export const {
    makeStyles,
    useCssAndCx,
    TssProviderForSsr, //<- This is what's new
} = createMakesStyles({ theme });

pages/index.tsx

import { createMakeStyle } from "tss-react";
import { TssProviderForSsr } from "../shared/styleEngine";

export default function Home() {
    return (
        <TssProviderForSsr>
            <App />
        </TssProviderForSsr>
    );
}

Backend configuration with Next.js

If you don't have a _document.tsx

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

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

export default Document;

If have have a _document.tsx but you haven't overloaded getInitialProps

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

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

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

If have have a _document.tsx and an overloaded getInitialProps

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

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...
}

Backend configuration general case.

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

import { cache } from "tss-react/cache";
import { createMakeStyle } from "tss-react";

const { extractCriticalToChunks, constructStyleTagsFromChunks } = createEmotionServer(cache);

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

const { TssProviderForSsr, makeStyles, useCssAndCx } = createMakeStyle({ useTheme });

export { makeStyles, useCssAndCx };

const element = (
    <TssProviderForSsr>
        <App />
    </TssProviderForSsr>
);

const { html, styles } = extractCriticalToChunks(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>
    ${constructStyleTagsFromChunks({ html, styles })}
</head>
<body>
    <div id="root">${html}</div>

    <script src="./bundle.js"></script>
</body>
</html>`);

Road map to v1