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 ( ) ;
18
14
19
15
// const measure = (name, fn) => {
20
16
// window.performance.mark(`${name}_start`)
@@ -30,184 +26,207 @@ const defaultInsertedRules = new Set()
30
26
31
27
// window.summarize = summarize
32
28
33
- const measure = ( name , fn ) => fn ( )
29
+ const measure = ( name , fn ) => fn ( ) ;
34
30
31
+ // Significantly more performant than `list.flat()`: https://stackoverflow.com/a/61416753
32
+ // TODO: explore manual looping
33
+ const flatten = ( list ) => [ ] . concat ( ...list ) ;
35
34
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 ] ;
43
38
44
- if ( cache [ name ] && cache [ name ] [ value ] ) {
45
- return cache [ name ] [ value ]
39
+ if ( existingCacheEntry ) {
40
+ return existingCacheEntry ;
46
41
}
47
42
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)
54
46
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 ] = { } ;
63
53
}
64
54
65
- cache [ name ] [ value ] = { id, className, name, value }
55
+ const cacheEntry = { id, className, psuedoClass , name, value } ;
66
56
67
- return cache [ name ] [ value ]
68
- } )
69
- }
57
+ cache [ psuedoClass ] [ name ] [ value ] = cacheEntry ;
70
58
59
+ return cacheEntry ;
60
+ } ) ;
61
+ } ;
71
62
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 = ( ) => { } ;
72
93
73
94
export const StylesProvider = ( {
74
95
children,
75
96
options = { } ,
76
97
initialCache = defaultCache ,
77
98
} ) => {
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
+ ) ;
80
105
81
106
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 ) ;
84
109
85
110
if ( existingElement ) {
86
- stylesheetRef . current = existingElement . sheet
87
- return
111
+ stylesheetRef . current = existingElement . sheet ;
112
+ return ;
88
113
}
89
114
90
- const element = window . document . createElement ( ' style' )
91
- element . id = id
115
+ const element = window . document . createElement ( " style" ) ;
116
+ element . id = id ;
92
117
93
- window . document . head . appendChild ( element )
118
+ window . document . head . appendChild ( element ) ;
94
119
95
- stylesheetRef . current = element . sheet
96
- } , [ ] )
120
+ stylesheetRef . current = element . sheet ;
121
+ } , [ ] ) ;
97
122
98
123
React . useEffect ( ( ) => {
99
124
if ( stylesheetRef . current ) {
100
125
return ;
101
126
}
102
- insertStylesheet ( )
127
+ insertStylesheet ( ) ;
103
128
// console.log('effect')
104
129
105
130
// return () => {
106
131
// // dom_.removeChild(window.document.body, element)
107
132
// }
108
- } , [ insertStylesheet ] )
133
+ } , [ insertStylesheet ] ) ;
109
134
110
135
return React . createElement (
111
136
// TODO: split contexts
112
137
cacheContext . Provider ,
113
138
{
114
139
value : {
115
140
insertRule : React . useCallback (
116
- ( { id, className, name, value } ) => {
141
+ ( { id, className, psuedoClass = "" , name, value } ) => {
117
142
if ( ! stylesheetRef . current ) {
118
- insertStylesheet ( )
143
+ insertStylesheet ( ) ;
119
144
}
120
145
121
146
if ( defaultInsertedRules . has ( id ) ) {
122
147
// console.log('cached rule', rule)
123
- return
148
+ return ;
124
149
}
125
150
126
151
if ( useCssTypedOm ) {
127
152
// CSS Typed OM unfortunately doesn't deal with stylesheets yet, but supposedy it's coming:
128
153
// 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 ) ;
132
157
} 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 ) ;
138
162
}
139
- // mutative cache for perf
140
- defaultInsertedRules . add ( id )
163
+ // mutative cache for perf
164
+ defaultInsertedRules . add ( id ) ;
141
165
} ,
142
- [ insertStylesheet , useCssTypedOm ] ,
166
+ [ insertStylesheet , useCssTypedOm ]
143
167
) ,
144
168
toCacheEntries : React . useCallback (
145
- ( styles ) => toCacheEntries ( { styles, cache : initialCache } ) ,
146
- [ ] ,
169
+ ( stylesEntries ) =>
170
+ toCacheEntriesLayer2 ( { stylesEntries, cache : initialCache } ) ,
171
+ [ ]
147
172
) ,
148
173
useCssTypedOm,
149
174
} ,
150
175
} ,
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
+ } ;
161
179
162
- const cache = React . useContext ( cacheContext )
180
+ export const useStylesEntries = (
181
+ stylesEntries
182
+ // { resolveStyle } = {},
183
+ ) => {
184
+ const cache = React . useContext ( cacheContext ) ;
163
185
164
186
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
+ ) ;
166
190
}
167
191
168
- const { insertRule, toCacheEntries } = cache
192
+ const { insertRule, toCacheEntries } = cache ;
169
193
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
+ ) ;
173
198
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 + " " ;
181
206
}
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
- // )
198
207
208
+ return classNames ;
209
+ } , [ cacheEntries ] )
210
+ ) ;
199
211
200
212
React . useLayoutEffect ( ( ) => {
201
- measure ( ' insert' , ( ) => {
202
- cacheEntries . forEach ( insertRule )
203
- } )
213
+ measure ( " insert" , ( ) => {
214
+ cacheEntries . forEach ( insertRule ) ;
215
+ } ) ;
204
216
// cacheEntries.forEach(insertRule)
205
217
206
218
return ( ) => {
207
219
// This is not necessary, and hinders performance
208
220
// 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
+ } ;
0 commit comments