✨ 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
.
- ✅ As fast as
emotion
(see the difference with mui'smakeStyles
) - ✅ As lightweight as
@emotion/react
. - ✅ Server side rendering support (e.g: Next.js).
- ✅ Seamless integration with material-ui v5.
Perfect for those who don't like the switch from the Hook API to the Styled API in v5. - ✅ Complete
@emotion
custom cache integration.
$ yarn add tss-react @emotion/react
./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";
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>;
}
Material-UI users only: Setup injection priority.
Click to expand instructions for material-ui v4.
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.
Click to expand instructions for material-ui v5
Don't use <StyledEngineProvider injectFirst/>
but do this instead:
import { render } from "react-dom";
import { CacheProvider } from "@emotion/react";
import { getCache } from "tss-react/cache";
render(
<CacheProvider value={getCache()}>
<Root />
</StyledEngineProvider>,
document.getElementById("root"),
);
Feel free to use any emotion cache you want.
You don't have to use the default one provided in tss-react/cache
.
NOTE:
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 yo your src/
directory.
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.
} from "tss-react";
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();
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>
);
}
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>
</>
);
}
// 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`,
},
},
});
If you are using custom emotion cache tss-react
will transparently
pick up the cache you have provided using <CacheProvider />
from @emotion/react
.
import { CacheProvider } from "@emotion/react";
import createCache from "@emotion/cache";
/* OR:
import createCache from "tss-react/@emotion/cache";
*/
const myCache = createCache({
"key": "my-prefix-key",
//...
});
render(<CacheProvider value={myCache}>{/* ... */}</CacheProvider>);
You can also opt for telling tss-react
to use a specific cache and ignore
the cache provided by the <CacheProvider />
.
import { createMakeStyles } from "tss-react";
import createCache from "@emotion/cache";
const { makeStyles } = createMakeStyles({
useTheme,
"cache": createCache({ "key": "my-prefix-key" }),
});
If there is no cache provided by <CacheProvider />
nor any cache specified
when calling createMakeStyles()
then the cache used is import { getCache } from "tss-react/cache"
.
tss-react
unlike jss-react
doesn't support the $
syntax,
but you'll see. It isn't needed.
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,
},
};
},
);
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...
}));
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
Just create a file page/_document.tsx
as follow:
import { createDocument } from "tss-react/nextJs";
const { Document } = createDocument();
/*
If you use custom cache you should provide it here:
const { Document } = createDocument({ "caches": [ cache1, cache2, ... ] });
*/
export default Document;
import Document from "next/document";
import type { DocumentContext } from "next/document";
import { createGetInitialProps } from "tss-react/nextJs";
const { getInitialProps } = createGetInitialProps();
/*
If you use custom cache you should provide it here:
const { getInitialProps } = createGetInitialProps({ "caches": [ cache1, cache2, ... ] });
*/
export default class AppDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
return getInitialProps(ctx);
}
//...Rest of your class...
}
import Document from "next/document";
import type { DocumentContext } from "next/document";
import { createPageHtmlToStyleTags } from "tss-react/nextJs";
const { pageHtmlToStyleTags } = createPageHtmlToStyleTags();
/*
If you use custom cache you should provide it here:
const { pageHtmlToStyleTags } = createPageHtmlToStyleTags({ "caches": [ cache1, cache2, ... ] });
*/
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...
}
yarn add @emotion/server
import { renderToString } from "react-dom/server";
import createEmotionServer from "@emotion/server/create-instance";
import { getCache } from "tss-react/cache";
import { createMakeStyles } from "tss-react";
const emotionServers = [
getCache(), //If you use custom cache(s) provide it/them here instead of the default.
].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>`);
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
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:
See this issue