Skip to content

Commit 5c1ec22

Browse files
committed
V2
- Breaking: Removed dependency list Added alternative implementation where performance is almost identical, but ergonomics is much improved. Can still useMemo for micro-optimizations on critical paths in user-space. Not quite a breaking change in practice, since dependency lists will now be ignored. But I do plan on adding an options bag to the same arg in v3, which will turn this into a breaking change. - Support for psuedoclasses Uses same API as in emotion object syntax. Next up: media queries & keyframes
1 parent 755ca0c commit 5c1ec22

File tree

11 files changed

+987
-160
lines changed

11 files changed

+987
-160
lines changed

.gitignore

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
11
/benchmarks/node_modules
22
/benchmarks/dist
3-
/node_modules
3+
/node_modules
4+
5+
.pnp.*
6+
.yarn/*
7+
!.yarn/patches
8+
!.yarn/plugins
9+
!.yarn/releases
10+
!.yarn/sdks
11+
!.yarn/versions

.yarn/releases/yarn-berry.cjs

+768
Large diffs are not rendered by default.

.yarnrc.yml

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
nodeLinker: node-modules
2+
3+
yarnPath: .yarn/releases/yarn-berry.cjs

package.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lewisl9029/use-styles",
3-
"version": "1.0.0",
3+
"version": "2.0.0",
44
"description": "Use styles",
55
"main": "src/index.js",
66
"repository": "https://github.com/lewisl9029/use-styles.git",
@@ -13,7 +13,6 @@
1313
"files": [
1414
"src/*"
1515
],
16-
"dependencies": {},
1716
"devDependencies": {
1817
"react": "^16.8.0"
1918
},

src/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { useStyles, StylesProvider } from './useStyles'
1+
export { useStyles, StylesProvider } from "./useStyles.js";

src/useStyles.js

+140-121
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
1-
import * as React from 'react'
2-
import hash from './hash.js'
3-
import hyphenate from './hyphenate.js'
4-
import withUnit from './withUnit.js'
5-
const cacheContext = React.createContext(undefined)
6-
7-
// TODO: support media queries or recommend component size queries instead?
8-
// TODO: are these even necessary? or should we recommend using react event handlers and state
9-
const supportedPseudoClasses = new Set([
10-
':hover',
11-
':focus',
12-
':focus-visible',
13-
':focus-within',
14-
])
15-
16-
const defaultCache = {}
17-
const defaultInsertedRules = new Set()
1+
import * as React from "react";
2+
import hash from "./useStyles/hash.js";
3+
import hyphenate from "./useStyles/hyphenate.js";
4+
import withUnit from "./useStyles/withUnit.js";
5+
import cacheContext from "./useStyles/cacheContext.js";
6+
7+
// TODO: other perf explorations:
8+
// - Replace array methods with manually optimized for loops
9+
// - Preallocate array size where feasible
10+
// - More low-level caching and memoization
11+
12+
const defaultCache = {};
13+
const defaultInsertedRules = new Set();
1814

1915
// const measure = (name, fn) => {
2016
// window.performance.mark(`${name}_start`)
@@ -30,184 +26,207 @@ const defaultInsertedRules = new Set()
3026

3127
// window.summarize = summarize
3228

33-
const measure = (name, fn) => fn()
29+
const measure = (name, fn) => fn();
3430

31+
// Significantly more performant than `list.flat()`: https://stackoverflow.com/a/61416753
32+
// TODO: explore manual looping
33+
const flatten = (list) => [].concat(...list);
3534

36-
const toCacheEntries = ({ styles, cache }) => {
37-
// const toCacheEntries = ({ styles, pseudoClass = '', cache }) => {
38-
// TODO: experiment with using single rule per styles
39-
return Object.entries(styles).map(([name, value]) => {
40-
// if (supportedPseudoClasses.has(name)) {
41-
// return toCacheEntries({ styles: value, pseudoClass: name, cache })
42-
// }
35+
const toCacheEntries = ({ stylesEntries, psuedoClass, cache }) => {
36+
return stylesEntries.map(([name, value]) => {
37+
const existingCacheEntry = cache[psuedoClass]?.[name]?.[value];
4338

44-
if (cache[name] && cache[name][value]) {
45-
return cache[name][value]
39+
if (existingCacheEntry) {
40+
return existingCacheEntry;
4641
}
4742

48-
// const id = `${name}_${value}`
49-
const id = measure('id', () => `${name}_${value}`)
50-
console.log("uncached rule: " + id)
51-
52-
// const className = `r_${hash(id)}`
53-
const className = measure('hash', () => `r_${hash(id)}`)
43+
// psuedoclass need to be a part of id to allow distinct targetting
44+
const id = measure("id", () => `${psuedoClass}_${name}_${value}`);
45+
// console.log('uncached rule: ' + id)
5446

55-
// const rule = `.${className} { ${styleName}: ${withUnit(
56-
// name,
57-
// value,
58-
// )}; }`
59-
60-
61-
if (!cache[name]) {
62-
cache[name] = {}
47+
const className = measure("hash", () => `r_${hash(id)}`);
48+
if (!cache[psuedoClass]) {
49+
cache[psuedoClass] = {};
50+
}
51+
if (!cache[psuedoClass][name]) {
52+
cache[psuedoClass][name] = {};
6353
}
6454

65-
cache[name][value] = { id, className, name, value }
55+
const cacheEntry = { id, className, psuedoClass, name, value };
6656

67-
return cache[name][value]
68-
})
69-
}
57+
cache[psuedoClass][name][value] = cacheEntry;
7058

59+
return cacheEntry;
60+
});
61+
};
7162

63+
// TODO: Psuedoclasses
64+
const toCacheEntriesLayer2 = ({ stylesEntries, cache }) => {
65+
const { withPsuedoClass, withoutPsuedoClass } = stylesEntries.reduce(
66+
(groups, entry) => {
67+
const key = entry[0];
68+
if (key[0] === ":") {
69+
groups.withPsuedoClass.push(entry);
70+
} else {
71+
groups.withoutPsuedoClass.push(entry);
72+
}
73+
return groups;
74+
},
75+
{ withPsuedoClass: [], withoutPsuedoClass: [] }
76+
);
77+
78+
return [
79+
...toCacheEntries({ stylesEntries: withoutPsuedoClass, cache }),
80+
...flatten(
81+
withPsuedoClass.map(([psuedoClass, styles]) =>
82+
toCacheEntries({
83+
stylesEntries: Object.entries(styles),
84+
psuedoClass,
85+
cache,
86+
})
87+
)
88+
),
89+
];
90+
};
91+
// TODO: support media queries or recommend component size queries instead? what about keyframes?
92+
const toCacheEntriesLayer3 = () => {};
7293

7394
export const StylesProvider = ({
7495
children,
7596
options = {},
7697
initialCache = defaultCache,
7798
}) => {
78-
const stylesheetRef = React.useRef()
79-
const useCssTypedOm = !!(options.useCssTypedOm && window.CSS && window.CSS.number)
99+
const stylesheetRef = React.useRef();
100+
const useCssTypedOm = !!(
101+
options.useCssTypedOm &&
102+
window.CSS &&
103+
window.CSS.number
104+
);
80105

81106
const insertStylesheet = React.useCallback(() => {
82-
const id = `useStylesStylesheet`
83-
const existingElement = window.document.getElementById(id)
107+
const id = `useStylesStylesheet`;
108+
const existingElement = window.document.getElementById(id);
84109

85110
if (existingElement) {
86-
stylesheetRef.current = existingElement.sheet
87-
return
111+
stylesheetRef.current = existingElement.sheet;
112+
return;
88113
}
89114

90-
const element = window.document.createElement('style')
91-
element.id = id
115+
const element = window.document.createElement("style");
116+
element.id = id;
92117

93-
window.document.head.appendChild(element)
118+
window.document.head.appendChild(element);
94119

95-
stylesheetRef.current = element.sheet
96-
},[])
120+
stylesheetRef.current = element.sheet;
121+
}, []);
97122

98123
React.useEffect(() => {
99124
if (stylesheetRef.current) {
100125
return;
101126
}
102-
insertStylesheet()
127+
insertStylesheet();
103128
// console.log('effect')
104129

105130
// return () => {
106131
// // dom_.removeChild(window.document.body, element)
107132
// }
108-
}, [insertStylesheet])
133+
}, [insertStylesheet]);
109134

110135
return React.createElement(
111136
// TODO: split contexts
112137
cacheContext.Provider,
113138
{
114139
value: {
115140
insertRule: React.useCallback(
116-
({ id, className, name, value }) => {
141+
({ id, className, psuedoClass = "", name, value }) => {
117142
if (!stylesheetRef.current) {
118-
insertStylesheet()
143+
insertStylesheet();
119144
}
120145

121146
if (defaultInsertedRules.has(id)) {
122147
// console.log('cached rule', rule)
123-
return
148+
return;
124149
}
125150

126151
if (useCssTypedOm) {
127152
// CSS Typed OM unfortunately doesn't deal with stylesheets yet, but supposedy it's coming:
128153
// https://github.com/w3c/css-houdini-drafts/issues/96#issuecomment-468063223
129-
const rule = `.${className} {}`
130-
const index = stylesheetRef.current.insertRule(rule)
131-
stylesheetRef.current.cssRules[index].styleMap.set(name, value)
154+
const rule = `.${className}${psuedoClass} {}`;
155+
const index = stylesheetRef.current.insertRule(rule);
156+
stylesheetRef.current.cssRules[index].styleMap.set(name, value);
132157
} else {
133-
const rule = `.${className} { ${hyphenate(name)}: ${withUnit(
134-
name,
135-
value,
136-
)}; }`
137-
stylesheetRef.current.insertRule(rule)
158+
const rule = `.${className}${psuedoClass} { ${hyphenate(
159+
name
160+
)}: ${withUnit(name, value)}; }`;
161+
stylesheetRef.current.insertRule(rule);
138162
}
139-
// mutative cache for perf
140-
defaultInsertedRules.add(id)
163+
// mutative cache for perf
164+
defaultInsertedRules.add(id);
141165
},
142-
[insertStylesheet, useCssTypedOm],
166+
[insertStylesheet, useCssTypedOm]
143167
),
144168
toCacheEntries: React.useCallback(
145-
(styles) => toCacheEntries({ styles, cache: initialCache }),
146-
[],
169+
(stylesEntries) =>
170+
toCacheEntriesLayer2({ stylesEntries, cache: initialCache }),
171+
[]
147172
),
148173
useCssTypedOm,
149174
},
150175
},
151-
children,
152-
)
153-
}
154-
155-
export const useStyles = (styles, dependencies) => {
156-
if (!dependencies) {
157-
console.warn(
158-
'styles will be reprocessed every render if a dependencies array is not provided, pass in an empty array if styles are static',
159-
)
160-
}
176+
children
177+
);
178+
};
161179

162-
const cache = React.useContext(cacheContext)
180+
export const useStylesEntries = (
181+
stylesEntries
182+
// { resolveStyle } = {},
183+
) => {
184+
const cache = React.useContext(cacheContext);
163185

164186
if (cache === undefined) {
165-
throw new Error('Please ensure usages of useStyles are contained within StylesProvider')
187+
throw new Error(
188+
"Please ensure usages of useStyles are contained within StylesProvider"
189+
);
166190
}
167191

168-
const { insertRule, toCacheEntries } = cache
192+
const { insertRule, toCacheEntries } = cache;
169193

170-
// const cacheEntries = React.useMemo(() => toCacheEntries(styles), dependencies)
171-
// console.log(dependencies)
172-
const cacheEntries = measure('cacheEntries', () => React.useMemo(() => toCacheEntries(styles), dependencies))
194+
const cacheEntries = measure(
195+
"toCacheEntries",
196+
React.useMemo(() => toCacheEntries(stylesEntries), [stylesEntries])
197+
);
173198

174-
const classNames = measure('classNames', () => React.useMemo(
175-
// () => cacheEntries.map(({ className }) => className).join(' '),
176-
() => {
177-
const length = cacheEntries.length
178-
let classNames = ''
179-
for(let index = 0; index < length; index++) {
180-
classNames+=cacheEntries[index].className + ' '
199+
const classNames = measure(
200+
"classNames",
201+
React.useMemo(() => {
202+
const length = cacheEntries.length;
203+
let classNames = "";
204+
for (let index = 0; index < length; index++) {
205+
classNames += cacheEntries[index].className + " ";
181206
}
182-
return classNames
183-
},
184-
[cacheEntries],
185-
))
186-
// const classNames = React.useMemo(
187-
// // () => cacheEntries.map(({ className }) => className).join(' '),
188-
// () => {
189-
// const length = cacheEntries.length
190-
// let classNames = ''
191-
// for(let index = 0; index < length; index++) {
192-
// classNames+=cacheEntries[index].className + ' '
193-
// }
194-
// return classNames
195-
// },
196-
// [cacheEntries],
197-
// )
198207

208+
return classNames;
209+
}, [cacheEntries])
210+
);
199211

200212
React.useLayoutEffect(() => {
201-
measure('insert', () => {
202-
cacheEntries.forEach(insertRule)
203-
})
213+
measure("insert", () => {
214+
cacheEntries.forEach(insertRule);
215+
});
204216
// cacheEntries.forEach(insertRule)
205217

206218
return () => {
207219
// This is not necessary, and hinders performance
208220
// stylesheet.deleteRule(index)
209-
}
210-
}, [cacheEntries])
211-
212-
return classNames
213-
}
221+
};
222+
}, [cacheEntries]);
223+
224+
// Add space to facilitate concatenation
225+
return classNames + " ";
226+
};
227+
228+
export const useStyles = (styles) => {
229+
return useStylesEntries(
230+
React.useMemo(() => Object.entries(styles), [styles])
231+
);
232+
};

src/useStyles/cacheContext.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import * as React from 'react'
2+
3+
const cacheContext = React.createContext(undefined)
4+
5+
export default cacheContext
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)