├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── lerna.json ├── package.json ├── packages ├── style-api-jss │ ├── README.md │ ├── package.json │ └── src │ │ ├── index.jsx │ │ └── style-utils.js ├── style-api │ ├── README.md │ ├── package.json │ └── src │ │ └── index.jsx └── style-hook │ ├── README.md │ ├── package.json │ └── src │ └── index.js ├── samples └── app │ ├── package.json │ └── src │ ├── index.html │ └── index.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | dist/ 3 | lib/ 4 | node_modules/ 5 | .DS_Store 6 | Thumbs.db 7 | *.log 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Andy Wermke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unified React styling hook 2 | 3 | Leveraging the [React Hooks API](https://reactjs.org/docs/hooks-intro.html) to provide an elegant, unified styling API. 4 | 5 | One simple API based on the hooks API to style all components. Suddenly it does not matter anymore if you are using Emotion, Styled Components or JSS - the styling becomes transparent. 6 | 7 | Great for component libraries: Don't force your users into a particular styling library! Style with universal hooks and let the users plug-in the styling library that works best for them. 8 | 9 | #### Features 10 | 11 | 💅 Style components with `useStyles()`
12 | 💡 Uncouple components from styling library
13 | 🌅 Server side rendering
14 | 🌈 Theming support out of the box
15 | 16 |
17 | 18 | **Any feedback welcome! Leave [a comment](https://github.com/andywer/react-usestyles/issues/2) or 🌟 the repo.** 19 | 20 | ⚠️  Attention: Bleeding edge ahead. Don't use this in production. 21 | 22 | 23 | ## Installation 24 | 25 | In your app: 26 | 27 | ```sh 28 | $ npm install react@next react-dom@next @andywer/style-hook @andywer/style-api-jss 29 | ``` 30 | 31 | In a component package you only need this: 32 | 33 | ```sh 34 | $ npm install react@next @andywer/style-hook 35 | ``` 36 | 37 | 38 | ## Live Playgrounds 39 | 40 | Here are some code sandboxes to see the style hooks in action. You can also see the source code and live-edit it: 41 | 42 | 😸 Basics -
43 | 🖥 Material-UI / Simple Hacker News client -
44 | 45 | 46 | ## Usage 47 | 48 | ### `useStyles()` 49 | 50 | ```jsx 51 | // Button.js 52 | import { useStyles } from "@andywer/style-hook" 53 | import React from "react" 54 | 55 | export function Button (props) { 56 | const classNames = useStyles({ 57 | button: { 58 | padding: "0.6em 1.2em", 59 | background: theme => theme.button.default.background, 60 | color: theme => theme.button.default.textColor, 61 | border: "none", 62 | boxShadow: "0 0 0.5em #b0b0b0", 63 | "&:hover": { 64 | boxShadow: "0 0 0.5em #e0e0e0" 65 | } 66 | }, 67 | buttonPrimary: { 68 | background: theme => theme.button.primary.background, 69 | color: theme => theme.button.primary.textColor 70 | } 71 | }) 72 | 73 | const className = `${classNames.button} ${props.primary ? classNames.buttonPrimary : ""}` 74 | return ( 75 | 78 | ) 79 | } 80 | ``` 81 | 82 | Rendering your app with style hook support is easy: 83 | Add a provider for the styling library at the top of your app. 84 | 85 | ```jsx 86 | // App.js 87 | import { JssProvider } from "@andywer/style-api-jss" 88 | import React from "react" 89 | import ReactDOM from "react-dom" 90 | 91 | const App = () => ( 92 | 93 | {/* ... */} 94 | 95 | ) 96 | 97 | ReactDOM.render(, document.getElementById("app")) 98 | ``` 99 | 100 | ### `useStyle()` 101 | 102 | This is a convenience function to define a single CSS class. 103 | 104 | ```jsx 105 | import { useStyle } from "@andywer/style-hook" 106 | import React from "react" 107 | 108 | export function Button (props) { 109 | const className = useStyle({ 110 | padding: "0.6em 1.2em", 111 | boxShadow: "0 0 0.5em #b0b0b0", 112 | "&:hover": { 113 | boxShadow: "0 0 0.5em #e0e0e0" 114 | } 115 | }) 116 | return ( 117 | 120 | ) 121 | } 122 | ``` 123 | 124 | ### Theming & props 125 | 126 | ```jsx 127 | import { useStyles } from "@andywer/style-hook" 128 | import React from "react" 129 | 130 | export function Button (props) { 131 | const classNames = useStyles({ 132 | button: { 133 | background: theme => theme.button.default.background, 134 | color: theme => theme.button.default.textColor, 135 | border: () => props.border || "none", 136 | boxShadow: "0 0 0.5em #b0b0b0", 137 | "&:hover": { 138 | boxShadow: "0 0 0.5em #e0e0e0" 139 | } 140 | }, 141 | buttonPrimary: { 142 | background: theme => theme.button.primary.background, 143 | color: theme => theme.button.primary.textColor 144 | } 145 | }, [props.border]) 146 | 147 | const className = `${classNames.button} ${props.primary ? classNames.buttonPrimary : ""}` 148 | return ( 149 | 152 | ) 153 | } 154 | ``` 155 | 156 | For details see the [`style-hook` package readme](https://github.com/andywer/react-usestyles/blob/master/packages/style-hook/README.md). 157 | 158 | 159 | ## Details 160 | 161 | **For more details about the API and usage instructions, check out the [`style-hook` package readme](./packages/style-hook/README.md).** 162 | 163 | 164 | ## API 165 | 166 | - [`style-hook`](./packages/style-hook) - The main package providing the hooks 167 | - [`style-api`](./packages/style-api) - Defines the API between styling library provider and hooks (internal) 168 | - [`style-api-jss`](./packages/style-api-jss) - The styling library provider for [JSS](http://cssinjs.org/) 169 | 170 | 171 | ## License 172 | 173 | MIT 174 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*", 4 | "samples/*" 5 | ], 6 | "version": "0.1.0" 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-use-styles", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "lerna run build", 7 | "watch": "lerna run watch", 8 | "release": "yarn build && lerna publish", 9 | "sample:serve": "cd samples/app/ && yarn dev" 10 | }, 11 | "workspaces": [ 12 | "packages/*", 13 | "samples/*" 14 | ], 15 | "devDependencies": { 16 | "@babel/cli": "^7.1.2", 17 | "@babel/core": "^7.1.2", 18 | "@babel/preset-env": "^7.1.0", 19 | "@babel/preset-react": "^7.0.0", 20 | "lerna": "^3.4.3", 21 | "react": "^16.7.0-alpha.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/style-api-jss/README.md: -------------------------------------------------------------------------------- 1 | # style-api-jss 2 | 3 | This package provides the glue to render the styles defined using the hooks using [JSS](http://cssinjs.org/). 4 | 5 | Check out the [project readme](https://github.com/andywer/react-usestyles) for more information. 6 | 7 | 8 | ## Installation 9 | 10 | ```sh 11 | $ npm install react@next react-dom@next @andywer/style-hook @andywer/style-api-jss 12 | ``` 13 | 14 | 15 | ## Usage 16 | 17 | ```jsx 18 | // App.js 19 | import { JssProvider } from "@andywer/style-api-jss" 20 | import { ThemeContext } from "@andywer/style-hook" 21 | import React from "react" 22 | import ReactDOM from "react-dom" 23 | import Button from "./Button" 24 | 25 | const myTheme = { 26 | button: { 27 | default: { 28 | background: "#ffffff", 29 | textColor: "#000000" 30 | }, 31 | primary: { 32 | background: "#3080ff", 33 | textColor: "#ffffff" 34 | } 35 | } 36 | } 37 | 38 | const App = () => ( 39 | 40 | 41 | 42 | 43 | 44 | ) 45 | 46 | ReactDOM.render(, document.getElementById("app")) 47 | ``` 48 | 49 | ## Server-side rendering 50 | 51 | ```jsx 52 | // App.js 53 | import { JssProvider, SheetsRegistry } from "@andywer/style-api-jss" 54 | import React from "react" 55 | import ReactDOM from "react-dom" 56 | 57 | const registry = new SheetsRegistry() 58 | 59 | const App = () => ( 60 | 61 | {/* ... */} 62 | 63 | ) 64 | 65 | const appHTML = ReactDOM.renderToString() 66 | const appCSS = registry.toString() 67 | ``` 68 | 69 | 70 | ## License 71 | 72 | MIT 73 | -------------------------------------------------------------------------------- /packages/style-api-jss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@andywer/style-api-jss", 3 | "version": "0.1.0", 4 | "author": "Andy Wermke (https://github.com/andywer)", 5 | "main": "lib/index.js", 6 | "license": "MIT", 7 | "repository": "andywer/react-usestyles", 8 | "scripts": { 9 | "build": "babel src/ -d lib/ --root-mode upward", 10 | "prepare": "build", 11 | "watch": "yarn build -- --watch" 12 | }, 13 | "peerDependencies": { 14 | "react": ">= 16.7.0 || >= 16.7.0-alpha.0" 15 | }, 16 | "dependencies": { 17 | "@andywer/style-api": "^0.1.0", 18 | "jss": "^9.8.7", 19 | "jss-preset-default": "^4.5.0" 20 | }, 21 | "files": [ 22 | "lib/**" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /packages/style-api-jss/src/index.jsx: -------------------------------------------------------------------------------- 1 | import { CssInJsProvider } from "@andywer/style-api" 2 | import { create, SheetsManager, SheetsRegistry } from "jss" 3 | import defaultPreset from "jss-preset-default" 4 | import React, { useState } from "react" 5 | import { getStaticStyles, isStaticStylesOnly, resolveStyles } from "./style-utils" 6 | 7 | export { SheetsRegistry } 8 | 9 | /** 10 | * Depending on the style hook's `inputs` argument (2nd argument), create a key 11 | * for the sheet, so that other useStyles() hook invocations working with the 12 | * same styles and updating in the same situations, share a key. 13 | * 14 | * That approach ensures that we can safely mutate the stylesheets. 15 | */ 16 | function createSheetMeta (styles, themeID, inputs) { 17 | const randomSheetID = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) 18 | 19 | if (Array.isArray(inputs) && inputs.length === 0) { 20 | if (isStaticStylesOnly(styles)) { 21 | // No function rules, no reaction to prop changes: Use stringified styles as key 22 | return { 23 | key: JSON.stringify(styles), 24 | isStatic: true 25 | } 26 | } else { 27 | // Function rules, but no reaction to prop changes: 28 | // Return a key that is the same for all similarly-styled React elements that use the same theme 29 | return { 30 | key: JSON.stringify(themeID) + ":" + JSON.stringify(getStaticStyles(styles)), 31 | isStatic: false 32 | } 33 | } 34 | } else { 35 | // Update styles on some specific or every prop changes: 36 | // Return always-unique key, since prop changes are unique to every React element 37 | return { 38 | key: randomSheetID, 39 | isStatic: false 40 | } 41 | } 42 | } 43 | 44 | function mutateJssSheet (jssSheet, updatedStyles) { 45 | const prevClassNames = Object.keys(jssSheet.classes) 46 | 47 | // TODO: Update only the rules that changed 48 | 49 | for (const prevClassName of prevClassNames) { 50 | jssSheet.deleteRule(prevClassName) 51 | } 52 | 53 | jssSheet.addRules(updatedStyles) 54 | } 55 | 56 | function createUnifiedSheet (meta, jss, manager, registry, styles, theme, jssSheetOptions) { 57 | const resolvedStyles = resolveStyles(styles, theme) 58 | const jssSheet = manager.get(meta.key) || jss.createStyleSheet(resolvedStyles, jssSheetOptions || {}) 59 | const latestStylesFingerprint = JSON.stringify(resolvedStyles) 60 | 61 | manager.add(meta.key, jssSheet) 62 | 63 | if (registry) { 64 | registry.add(jssSheet) 65 | } 66 | 67 | const sheet = { 68 | attached: false, 69 | latestStylesFingerprint, 70 | meta, 71 | 72 | attach () { 73 | manager.manage(meta.key) 74 | sheet.attached = true 75 | }, 76 | detach () { 77 | manager.unmanage(meta.key) 78 | sheet.attached = false 79 | }, 80 | getClassNames () { 81 | return jssSheet.classes 82 | }, 83 | toString () { 84 | return jssSheet.toString() 85 | }, 86 | update (currentStyles, currentTheme) { 87 | // TODO: Only when in development: 88 | // Warn if getStaticStyles(currentStyles) does not match getStaticStyles(styles) 89 | 90 | if (sheet.meta.isStatic) return 91 | 92 | const updatedResolvedStyles = resolveStyles(currentStyles, currentTheme) 93 | const updatedStylesFingerprint = JSON.stringify(updatedResolvedStyles) 94 | 95 | if (updatedStylesFingerprint === sheet.latestStylesFingerprint) return 96 | 97 | mutateJssSheet(jssSheet, updatedResolvedStyles) 98 | sheet.latestStylesFingerprint = updatedStylesFingerprint 99 | } 100 | } 101 | return sheet 102 | } 103 | 104 | export function JssProvider (props) { 105 | const [{ jss, manager }] = useState({ 106 | jss: create(defaultPreset()), 107 | manager: new SheetsManager() 108 | }) 109 | 110 | const createSheet = (styles, themeID, theme, inputs) => { 111 | const sheetMeta = createSheetMeta(styles, themeID, inputs) 112 | return createUnifiedSheet(sheetMeta, jss, manager, props.registry, styles, theme, props.sheetOptions || {}) 113 | } 114 | 115 | return ( 116 | 117 | {props.children} 118 | 119 | ) 120 | } 121 | -------------------------------------------------------------------------------- /packages/style-api-jss/src/style-utils.js: -------------------------------------------------------------------------------- 1 | export function getStaticStyles (styles) { 2 | const statics = {} 3 | 4 | for (const key of Object.keys(styles)) { 5 | const value = styles[key] 6 | if (typeof value === "object" && value) { 7 | statics[key] = getStaticStyles(value) 8 | } else if (typeof value !== "function") { 9 | statics[key] = value 10 | } 11 | } 12 | 13 | return statics 14 | } 15 | 16 | export function isStaticStylesOnly (styles) { 17 | for (const key of Object.keys(styles)) { 18 | const value = styles[key] 19 | if (typeof value === "object" && value) { 20 | if (!isStaticStylesOnly(value)) return false 21 | } else if (typeof value === "function") { 22 | return false 23 | } 24 | } 25 | return true 26 | } 27 | 28 | export function resolveStyles (styles, theme) { 29 | const resolved = {} 30 | 31 | for (const key of Object.keys(styles)) { 32 | const value = styles[key] 33 | if (typeof value === "object" && value) { 34 | resolved[key] = resolveStyles(value, theme) 35 | } else if (typeof value === "function") { 36 | resolved[key] = value(theme) 37 | } else { 38 | resolved[key] = value 39 | } 40 | } 41 | 42 | return resolved 43 | } 44 | -------------------------------------------------------------------------------- /packages/style-api/README.md: -------------------------------------------------------------------------------- 1 | # style-api 2 | 3 | This package is for internal use only. It defines the API between the hooks and the styling library providers. 4 | 5 | Check out the [project readme](https://github.com/andywer/react-usestyles) for more information. 6 | 7 | 8 | ## License 9 | 10 | MIT 11 | -------------------------------------------------------------------------------- /packages/style-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@andywer/style-api", 3 | "version": "0.1.0", 4 | "author": "Andy Wermke (https://github.com/andywer)", 5 | "license": "MIT", 6 | "main": "lib/index.js", 7 | "repository": "andywer/react-usestyles", 8 | "scripts": { 9 | "build": "babel src/ -d lib/ --root-mode upward", 10 | "prepare": "build", 11 | "watch": "yarn build -- --watch" 12 | }, 13 | "peerDependencies": { 14 | "react": ">= 16.7.0 || >= 16.7.0-alpha.0" 15 | }, 16 | "files": [ 17 | "lib/**" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/style-api/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState } from "react" 2 | 3 | export const CssInJsContext = createContext() 4 | 5 | export function CssInJsProvider (props) { 6 | const [state] = useState({ 7 | createSheet: props.createSheet 8 | }) 9 | return ( 10 | 11 | {props.children} 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /packages/style-hook/README.md: -------------------------------------------------------------------------------- 1 | # style-hook 2 | 3 | This is the main user-facing package. It contains the hooks: `useStyles()`, `useStyle()`, `useGlobalStyles()`. 4 | 5 | Check out the [project readme](https://github.com/andywer/react-usestyles) for more information. 6 | 7 | 8 | ## Installation 9 | 10 | ```sh 11 | $ npm install react@next @andywer/style-hook 12 | ``` 13 | 14 | 15 | ## Usage 16 | 17 | ### `useStyles()` 18 | 19 | ```jsx 20 | // Button.js 21 | import { useStyles } from "@andywer/style-hook" 22 | import React from "react" 23 | 24 | export function Button (props) { 25 | const classNames = useStyles({ 26 | button: { 27 | padding: "0.6em 1.2em", 28 | border: "none", 29 | boxShadow: "0 0 0.5em #b0b0b0", 30 | "&:hover": { 31 | boxShadow: "0 0 0.5em #e0e0e0" 32 | } 33 | } 34 | }) 35 | 36 | return ( 37 | 40 | ) 41 | } 42 | ``` 43 | 44 | Will yield the following CSS: 45 | 46 | ```css 47 | .button-0-1-2 { 48 | border: none; 49 | padding: 0.6em 1.2em; 50 | background: #3080ff; 51 | box-shadow: 0 0 0.5em #b0b0b0; 52 | } 53 | .button-0-1-2:hover { 54 | box-shadow: 0 0 0.5em #e0e0e0; 55 | } 56 | ``` 57 | 58 | 59 | ### `useStyle()` 60 | 61 | This is a convenience function for when you just need to define one CSS class. This is a very frequent use case. 62 | 63 | ```jsx 64 | // Button.js 65 | import { useStyle } from "@andywer/style-hook" 66 | import React from "react" 67 | 68 | export function Button (props) { 69 | const className = useStyle({ 70 | padding: "0.6em 1.2em", 71 | border: "none", 72 | boxShadow: "0 0 0.5em #b0b0b0", 73 | "&:hover": { 74 | boxShadow: "0 0 0.5em #e0e0e0" 75 | } 76 | }) 77 | return ( 78 | 81 | ) 82 | } 83 | ``` 84 | 85 | 86 | ### Dynamic styles & themes 87 | 88 | ```jsx 89 | // Button.js 90 | import { useStyles } from "@andywer/style-hook" 91 | import React from "react" 92 | 93 | export function Button (props) { 94 | const classNames = useStyles({ 95 | button: { 96 | padding: "0.6em 1.2em", 97 | background: theme => theme.button.default.background, 98 | color: theme => theme.button.default.textColor, 99 | border: () => props.border || "none", 100 | boxShadow: "0 0 0.5em #b0b0b0", 101 | "&:hover": { 102 | boxShadow: "0 0 0.5em #e0e0e0" 103 | } 104 | }, 105 | buttonPrimary: { 106 | background: theme => theme.button.primary.background, 107 | color: theme => theme.button.primary.textColor 108 | } 109 | }, [props.border]) 110 | 111 | const className = [classNames.button, props.primary && classNames.buttonPrimary].join(" ") 112 | return ( 113 | 116 | ) 117 | } 118 | ``` 119 | 120 | All dynamic style rules (that means all rules in a style object that can potentially change during runtime) **MUST** have function values. Those functions receive the current theme (see `App` below) as argument and return the style rule value. 121 | 122 | You might be wondering what the second argument, the array, passed to `useStyles()` is good for. It is an optimization that is common among React hooks: Pass a list of all variables that this style object's dynamic rules depend on. 123 | 124 | If such an array is provided, the expensive CSS update will only be performed if one of those listed variables changed. The styles will always be updated if another theme is selected (see `App` below). Pass an empty array to indicate that a style update is only necessary on theme change. 125 | 126 | 127 | ### `useGlobalStyles()` 128 | 129 | ```jsx 130 | // BodyStyles.js 131 | import { useGlobalStyles } from "@andywer/style-hook" 132 | import React from "react" 133 | 134 | export function BodyStyles (props) { 135 | useGlobalStyles({ 136 | "body": { 137 | margin: 0, 138 | padding: 0 139 | }, 140 | "#app": { 141 | width: "100%", 142 | height: "100%", 143 | minHeight: "100vh" 144 | } 145 | }, []) 146 | return props.children || null 147 | } 148 | ``` 149 | 150 | ### App 151 | 152 | ```jsx 153 | // App.js 154 | import { JssProvider } from "@andywer/style-api-jss" 155 | import { ThemeContext } from "@andywer/style-hook" 156 | import React from "react" 157 | import ReactDOM from "react-dom" 158 | import Button from "./Button" 159 | 160 | const myTheme = { 161 | button: { 162 | default: { 163 | background: "#ffffff", 164 | textColor: "#000000" 165 | }, 166 | primary: { 167 | background: "#3080ff", 168 | textColor: "#ffffff" 169 | } 170 | } 171 | } 172 | 173 | const App = () => ( 174 | 175 | 176 | 177 | 178 | 179 | ) 180 | 181 | ReactDOM.render(, document.getElementById("app")) 182 | ``` 183 | 184 | The `JssProvider` that renders the styles using [JSS](https://github.com/cssinjs/react-jss) has been implemented as a first proof-of-concept styling library integration. 185 | 186 | 187 | ### Server-side rendering 188 | 189 | Works the same way as it always did with the styling library (in this case JSS) you use. 190 | 191 | ```jsx 192 | // App.js 193 | import { JssProvider, SheetsRegistry } from "@andywer/style-api-jss" 194 | import React from "react" 195 | import ReactDOM from "react-dom" 196 | 197 | const registry = new SheetsRegistry() 198 | 199 | const App = () => ( 200 | 201 | {/* ... */} 202 | 203 | ) 204 | 205 | const appHTML = ReactDOM.renderToString() 206 | const appCSS = registry.toString() 207 | ``` 208 | 209 | 210 | ## Open questions 211 | 212 | ### CSS injection order 213 | 214 | A big topic in CSS-in-JS is the injection order: The order in which to write component styles to the stylesheets in the DOM. Even with plain old CSS styles you have to think about the order of your styles if multiple selectors might match the same element. 215 | 216 | The default opinion in the community seems to be that an injection order based on component render order will not be stable enough and is likely to lead to styling issues. There are ways to define the injection order based on module resolution order instead, but they imply providing a less friendly API to the user. 217 | 218 | So this project is build around the idea of injecting styles based on render order nevertheless. The same issue seemed to be [one of the earliest issues in styled-components](https://github.com/styled-components/styled-components/issues/1) as well, but at least at that time they came to the conclusion that those edge cases are only hurtful if the user tries to implement styling anti-patterns. 219 | 220 | Only use the CSS class names returned by `useStyles()`, don't try to add selectors that match certain child tags. You should be fine 😉 221 | 222 | Use the styling hook, use it in different scenarios and try to break it. If you manage to break it whithout implementing anti-patterns, please open an issue and share 🐛 223 | 224 | 225 | ## License 226 | 227 | MIT 228 | -------------------------------------------------------------------------------- /packages/style-hook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@andywer/style-hook", 3 | "version": "0.1.1", 4 | "author": "Andy Wermke (https://github.com/andywer)", 5 | "main": "lib/index.js", 6 | "license": "MIT", 7 | "repository": "andywer/react-usestyles", 8 | "scripts": { 9 | "build": "babel src/ -d lib/ --root-mode upward", 10 | "prepare": "build", 11 | "watch": "yarn build -- --watch" 12 | }, 13 | "peerDependencies": { 14 | "@andywer/style-api": "0.x", 15 | "react": ">= 16.7.0 || >= 16.7.0-alpha.0" 16 | }, 17 | "dependencies": { 18 | "theming": "^2.1.2" 19 | }, 20 | "files": [ 21 | "lib/**" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /packages/style-hook/src/index.js: -------------------------------------------------------------------------------- 1 | import { CssInJsContext } from "@andywer/style-api" 2 | import { useContext, useMemo, useMutationEffect, useState } from "react" 3 | import { ThemeContext } from "theming" 4 | 5 | export { ThemeContext } 6 | 7 | function useStylesInternal (styles, inputs) { 8 | const cssInJs = useContext(CssInJsContext) 9 | const theme = useContext(ThemeContext) || {} 10 | 11 | if (!cssInJs) { 12 | throw new Error("No CSS-in-JS implementation found in context.") 13 | } 14 | if (!theme._id) { 15 | // Hacky! Just give every theme we see an ID, so we can tell them apart easily 16 | theme._id = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) 17 | } 18 | 19 | const [sheet] = useState(() => cssInJs.createSheet(styles, theme._id, theme, inputs)) 20 | 21 | useMutationEffect(() => { 22 | sheet.attach() 23 | return () => sheet.detach() 24 | }, []) 25 | 26 | // Misusing useMemo here to synchronously sheet.update() only if styles or theme changed 27 | useMemo(() => { 28 | if (sheet.attached) { 29 | sheet.update(styles, theme) 30 | } 31 | }, inputs ? [theme, ...inputs] : [theme, Math.random()]) 32 | 33 | return sheet.getClassNames() 34 | } 35 | 36 | function transformIntoGlobalStyles (styles) { 37 | const transformed = {} 38 | 39 | for (const key of Object.keys(styles)) { 40 | transformed[`@global ${key}`] = styles[key] 41 | } 42 | 43 | return transformed 44 | } 45 | 46 | function wrapStyleCallback (styleCallback, transformStyles) { 47 | // Don't just pass-through arbitrary arguments, since we check function.length in useStyles() 48 | if (styleCallback.length === 0) { 49 | return () => transformStyles(styleCallback()) 50 | } else if (styleCallback.length === 1) { 51 | return (theme) => transformStyles(styleCallback(theme)) 52 | } else if (styleCallback.length === 2) { 53 | return (theme, props) => transformStyles(styleCallback(theme, props)) 54 | } else { 55 | return (...args) => transformStyles(styleCallback(...args)) 56 | } 57 | } 58 | 59 | export function useStyles (styles, inputs = undefined) { 60 | return useStylesInternal(styles, inputs) 61 | } 62 | 63 | export function useStyle (style, inputs = undefined) { 64 | return useStylesInternal({ style }, inputs).style 65 | } 66 | 67 | export function useGlobalStyles (styles, inputs = undefined) { 68 | const transformedStyles = transformIntoGlobalStyles(styles) 69 | useStylesInternal(transformedStyles, inputs) 70 | } 71 | -------------------------------------------------------------------------------- /samples/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "parcel serve src/index.html" 7 | }, 8 | "dependencies": { 9 | "@andywer/style-api-jss": "^0.1.0", 10 | "@andywer/style-hook": "^0.1.0", 11 | "react": "^16.7.0-alpha.0", 12 | "react-dom": "^16.7.0-alpha.0" 13 | }, 14 | "devDependencies": { 15 | "parcel-bundler": "^1.10.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /samples/app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /samples/app/src/index.js: -------------------------------------------------------------------------------- 1 | import { useGlobalStyles, useStyle, useStyles, ThemeContext } from "@andywer/style-hook" 2 | import { JssProvider, SheetsRegistry } from "@andywer/style-api-jss" 3 | import React, { useState } from "react" 4 | import ReactDOM from "react-dom" 5 | 6 | const themeDark = { 7 | body: { 8 | background: "#505050" 9 | }, 10 | button: { 11 | default: { 12 | background: "#303030", 13 | textColor: "#ffffff" 14 | }, 15 | primary: { 16 | background: "#303030", 17 | textColor: "#f65151" 18 | } 19 | } 20 | } 21 | 22 | const themeLight = { 23 | body: { 24 | background: "#ffffff" 25 | }, 26 | button: { 27 | default: { 28 | background: "#ffffff", 29 | textColor: "#000000" 30 | }, 31 | primary: { 32 | background: "#3080ff", 33 | textColor: "#ffffff" 34 | } 35 | } 36 | } 37 | 38 | function GlobalStyles () { 39 | useGlobalStyles({ 40 | "body": { 41 | margin: 0, 42 | padding: 0, 43 | background: theme => theme.body.background, 44 | transition: "background .2s" 45 | }, 46 | "#app": { 47 | width: "100%", 48 | height: "100%", 49 | minHeight: "100vh" 50 | } 51 | }, []) 52 | return null 53 | } 54 | 55 | function Button (props) { 56 | const classNames = useStyles({ 57 | default: { 58 | padding: "0.6em 1.2em", 59 | background: theme => theme.button.default.background, 60 | color: theme => theme.button.default.textColor, 61 | cursor: "pointer", 62 | border: "none", 63 | boxShadow: "0 0 0.5em #b0b0b0", 64 | fontSize: 14, 65 | "&:hover": { 66 | boxShadow: "0 0 0.5em #e0e0e0" 67 | }, 68 | "&:focus": { 69 | boxShadow: "0 0 0.5em #e0e0e0", 70 | outline: "none" 71 | } 72 | }, 73 | primary: { 74 | background: theme => theme.button.primary.background, 75 | color: theme => theme.button.primary.textColor 76 | } 77 | }, []) 78 | 79 | const className = [ 80 | classNames.default, 81 | props.primary && classNames.primary, 82 | props.className || "" 83 | ].join(" ") 84 | return ( 85 | 88 | ) 89 | } 90 | 91 | function StrangeButton (props) { 92 | const className = useStyle({ background: "green" }) 93 | return ( 94 | 97 | ) 98 | } 99 | 100 | function Container (props) { 101 | const className = useStyle(() => props.css, [props.css]) 102 | return ( 103 |
104 | {props.children} 105 |
106 | ) 107 | } 108 | 109 | function FullSizeContainer (props) { 110 | return ( 111 | 112 | {props.children} 113 | 114 | ) 115 | } 116 | 117 | function App () { 118 | const [themeName, setActiveTheme] = useState("light") 119 | const toggleTheme = () => setActiveTheme(themeName === "light" ? "dark" : "light") 120 | return ( 121 | 122 | 123 | 124 | 125 | 126 | Strange button 127 | 128 | 131 | 134 | 135 | 136 | 137 | ) 138 | } 139 | 140 | window.registry = new SheetsRegistry() 141 | 142 | ReactDOM.render(, document.getElementById("app")) 143 | 144 | console.log("Aggregated styles for SSR:\n", window.registry.toString()) 145 | --------------------------------------------------------------------------------