@tamagui/web 2.3.3 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/cjs/createComponent.native.js +1 -1
- package/dist/cjs/createComponent.native.js.map +1 -1
- package/dist/cjs/helpers/getSplitStyles.cjs +5 -0
- package/dist/cjs/helpers/getSplitStyles.native.js +9 -0
- package/dist/cjs/helpers/getSplitStyles.native.js.map +1 -1
- package/dist/cjs/helpers/pointerEvents.native.js +26 -20
- package/dist/cjs/helpers/pointerEvents.native.js.map +1 -1
- package/dist/cjs/hooks/useComponentState.cjs +25 -1
- package/dist/cjs/hooks/useComponentState.native.js +25 -1
- package/dist/cjs/hooks/useComponentState.native.js.map +1 -1
- package/dist/cjs/hooks/useMedia.cjs +107 -42
- package/dist/cjs/hooks/useMedia.native.js +149 -60
- package/dist/cjs/hooks/useMedia.native.js.map +1 -1
- package/dist/cjs/hooks/useTheme.cjs +17 -4
- package/dist/cjs/hooks/useTheme.native.js +18 -4
- package/dist/cjs/hooks/useTheme.native.js.map +1 -1
- package/dist/cjs/hooks/useThemeState.cjs +132 -67
- package/dist/cjs/hooks/useThemeState.native.js +144 -70
- package/dist/cjs/hooks/useThemeState.native.js.map +1 -1
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.native.js +2 -0
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/views/Theme.cjs +1 -1
- package/dist/cjs/views/Theme.native.js +1 -1
- package/dist/cjs/views/Theme.native.js.map +1 -1
- package/dist/esm/createComponent.native.js +2 -2
- package/dist/esm/createComponent.native.js.map +1 -1
- package/dist/esm/helpers/getSplitStyles.mjs +5 -0
- package/dist/esm/helpers/getSplitStyles.mjs.map +1 -1
- package/dist/esm/helpers/getSplitStyles.native.js +9 -0
- package/dist/esm/helpers/getSplitStyles.native.js.map +1 -1
- package/dist/esm/helpers/pointerEvents.native.js +26 -20
- package/dist/esm/helpers/pointerEvents.native.js.map +1 -1
- package/dist/esm/hooks/useComponentState.mjs +26 -2
- package/dist/esm/hooks/useComponentState.mjs.map +1 -1
- package/dist/esm/hooks/useComponentState.native.js +26 -2
- package/dist/esm/hooks/useComponentState.native.js.map +1 -1
- package/dist/esm/hooks/useMedia.mjs +108 -43
- package/dist/esm/hooks/useMedia.mjs.map +1 -1
- package/dist/esm/hooks/useMedia.native.js +150 -61
- package/dist/esm/hooks/useMedia.native.js.map +1 -1
- package/dist/esm/hooks/useTheme.mjs +17 -4
- package/dist/esm/hooks/useTheme.mjs.map +1 -1
- package/dist/esm/hooks/useTheme.native.js +18 -4
- package/dist/esm/hooks/useTheme.native.js.map +1 -1
- package/dist/esm/hooks/useThemeState.mjs +133 -68
- package/dist/esm/hooks/useThemeState.mjs.map +1 -1
- package/dist/esm/hooks/useThemeState.native.js +145 -71
- package/dist/esm/hooks/useThemeState.native.js.map +1 -1
- package/dist/esm/index.js +3 -3
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/index.mjs +3 -3
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +3 -3
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/views/Theme.mjs +1 -1
- package/dist/esm/views/Theme.mjs.map +1 -1
- package/dist/esm/views/Theme.native.js +1 -1
- package/dist/esm/views/Theme.native.js.map +1 -1
- package/package.json +13 -13
- package/src/createComponent.tsx +8 -2
- package/src/helpers/getSplitStyles.tsx +33 -0
- package/src/helpers/pointerEvents.native.ts +26 -19
- package/src/hooks/useComponentState.ts +39 -2
- package/src/hooks/useMedia.tsx +147 -42
- package/src/hooks/useTheme.tsx +27 -5
- package/src/hooks/useThemeState.ts +263 -137
- package/src/index.ts +7 -1
- package/src/types.tsx +53 -5
- package/src/views/Theme.tsx +4 -1
- package/types/createComponent.d.ts.map +1 -1
- package/types/helpers/getSplitStyles.d.ts.map +1 -1
- package/types/helpers/pointerEvents.native.d.ts.map +1 -1
- package/types/hooks/useComponentState.d.ts +1 -1
- package/types/hooks/useComponentState.d.ts.map +1 -1
- package/types/hooks/useMedia.d.ts.map +1 -1
- package/types/hooks/useTheme.d.ts +1 -1
- package/types/hooks/useTheme.d.ts.map +1 -1
- package/types/hooks/useThemeState.d.ts +1 -1
- package/types/hooks/useThemeState.d.ts.map +1 -1
- package/types/index.d.ts +2 -2
- package/types/index.d.ts.map +1 -1
- package/types/types.d.ts +3 -1
- package/types/types.d.ts.map +1 -1
- package/types/views/Theme.d.ts.map +1 -1
package/src/hooks/useMedia.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isServer, isWeb } from '@tamagui/constants'
|
|
2
|
-
import {
|
|
2
|
+
import { useEffect, useReducer, useRef } from 'react'
|
|
3
3
|
import { getSetting } from '../config'
|
|
4
4
|
import { resetMediaStyleCache } from '../helpers/createMediaStyle'
|
|
5
5
|
import { matchMedia } from '../helpers/matchMedia'
|
|
@@ -66,6 +66,8 @@ export const configureMedia = (config: TamaguiInternalConfig) => {
|
|
|
66
66
|
mediaVersion++
|
|
67
67
|
// reset cached media style prefixes/selectors so they get recalculated with new key order
|
|
68
68
|
resetMediaStyleCache()
|
|
69
|
+
// touch-tracker getter object depends on the current media key set
|
|
70
|
+
resetMediaTouchTracker()
|
|
69
71
|
for (const key in media) {
|
|
70
72
|
getMedia()[key] = mediaQueryDefaultActive?.[key] || false
|
|
71
73
|
mediaKeys.add(`$${key}`)
|
|
@@ -136,6 +138,47 @@ type MediaState = {
|
|
|
136
138
|
|
|
137
139
|
const States = new WeakMap<any, MediaState>()
|
|
138
140
|
|
|
141
|
+
// shared "touch tracker" prototype: one object whose enumerable getter
|
|
142
|
+
// properties are pre-defined for every configured media key. Hermes inlines
|
|
143
|
+
// getter calls; the old `new Proxy(state, { get })` path forced an interpreted
|
|
144
|
+
// trap on every access — the dominant per-component cost in benchmarks. Each
|
|
145
|
+
// component owns just an Object.create(proto) with a Symbol-keyed slot
|
|
146
|
+
// pointing at its tracking set + current snapshot.
|
|
147
|
+
type MediaRefSlot = {
|
|
148
|
+
proxyTarget: MediaQueryState
|
|
149
|
+
keys: Set<string>
|
|
150
|
+
}
|
|
151
|
+
let touchTrackerProto: object | null = null
|
|
152
|
+
const refSlot = Symbol('mediaRefSlot')
|
|
153
|
+
|
|
154
|
+
function buildTouchTrackerProto(): object {
|
|
155
|
+
const proto: PropertyDescriptorMap = {}
|
|
156
|
+
for (const fullKey of mediaKeys) {
|
|
157
|
+
const key = fullKey[0] === '$' ? fullKey.slice(1) : fullKey
|
|
158
|
+
proto[key] = {
|
|
159
|
+
enumerable: true,
|
|
160
|
+
configurable: true,
|
|
161
|
+
get(this: { [refSlot]: MediaRefSlot }) {
|
|
162
|
+
const slot = this[refSlot]
|
|
163
|
+
if (!disableMediaTouch) {
|
|
164
|
+
slot.keys.add(key)
|
|
165
|
+
}
|
|
166
|
+
return slot.proxyTarget[key]
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return Object.create(null, proto)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getTouchTrackerProto(): object {
|
|
174
|
+
if (!touchTrackerProto) touchTrackerProto = buildTouchTrackerProto()
|
|
175
|
+
return touchTrackerProto
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function resetMediaTouchTracker() {
|
|
179
|
+
touchTrackerProto = null
|
|
180
|
+
}
|
|
181
|
+
|
|
139
182
|
export function setMediaShouldUpdate(
|
|
140
183
|
ref: any,
|
|
141
184
|
enabled?: boolean,
|
|
@@ -165,38 +208,46 @@ export function useMedia(
|
|
|
165
208
|
): UseMediaState {
|
|
166
209
|
'use no memo'
|
|
167
210
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const internalRef = useRef<{
|
|
211
|
+
type MediaRef = {
|
|
171
212
|
keys: Set<string>
|
|
172
213
|
lastState: MediaQueryState
|
|
173
214
|
pendingState?: MediaQueryState
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
internalRef.current.pendingState = undefined
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const { keys } = internalRef.current
|
|
189
|
-
|
|
190
|
-
// clear each render to track only rendered touched keys
|
|
191
|
-
if (keys.size) {
|
|
192
|
-
keys.clear()
|
|
215
|
+
renderVersion: number
|
|
216
|
+
unsubscribe?: () => void
|
|
217
|
+
// stable per-component closures + reusable Proxy. allocating new ones each
|
|
218
|
+
// render (via useSyncExternalStore + `new Proxy(state, ...)`) was a real
|
|
219
|
+
// per-component-per-render cost; we hold one Proxy whose target is swapped
|
|
220
|
+
// by mutating `proxyTarget` and re-reading it in the get trap.
|
|
221
|
+
proxyTarget: MediaQueryState
|
|
222
|
+
proxy: UseMediaState
|
|
223
|
+
getSnapshot: () => MediaQueryState
|
|
224
|
+
componentContext?: ComponentContextI
|
|
225
|
+
debug?: DebugProp
|
|
193
226
|
}
|
|
194
227
|
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
()
|
|
198
|
-
|
|
199
|
-
|
|
228
|
+
const internalRef = useRef<MediaRef | null>(null)
|
|
229
|
+
if (!internalRef.current) {
|
|
230
|
+
const initial = getMedia()
|
|
231
|
+
const r: MediaRef = {
|
|
232
|
+
keys: new Set<string>(),
|
|
233
|
+
lastState: initial,
|
|
234
|
+
renderVersion: 0,
|
|
235
|
+
proxyTarget: initial,
|
|
236
|
+
proxy: undefined as unknown as UseMediaState,
|
|
237
|
+
getSnapshot: undefined as unknown as () => MediaQueryState,
|
|
238
|
+
componentContext,
|
|
239
|
+
debug,
|
|
240
|
+
}
|
|
241
|
+
// proxy → Object.create(getterProto) with a Symbol slot. Per-key get is a
|
|
242
|
+
// monomorphic getter call (Hermes-fast) instead of a Proxy trap.
|
|
243
|
+
const tracker = Object.create(getTouchTrackerProto())
|
|
244
|
+
tracker[refSlot] = { proxyTarget: initial, keys: r.keys } as MediaRefSlot
|
|
245
|
+
r.proxy = tracker as UseMediaState
|
|
246
|
+
r.getSnapshot = () => {
|
|
247
|
+
const curKeys = r.componentContext
|
|
248
|
+
? States.get(r.componentContext)?.keys || r.keys
|
|
249
|
+
: r.keys
|
|
250
|
+
const { lastState, pendingState } = r
|
|
200
251
|
|
|
201
252
|
if (!curKeys.size) {
|
|
202
253
|
return lastState
|
|
@@ -205,38 +256,92 @@ export function useMedia(
|
|
|
205
256
|
const ms = getMedia()
|
|
206
257
|
for (const key of curKeys) {
|
|
207
258
|
if (ms[key] !== (pendingState || lastState)[key]) {
|
|
208
|
-
if (process.env.NODE_ENV === 'development' && debug) {
|
|
259
|
+
if (process.env.NODE_ENV === 'development' && r.debug) {
|
|
209
260
|
console.warn(`useMedia() ✍️`, key, lastState[key], '=>', ms[key])
|
|
210
261
|
}
|
|
211
262
|
|
|
212
263
|
// in emitter mode (no-rerender) avoid changing state, instead emit
|
|
213
|
-
if (componentContext?.mediaEmit) {
|
|
214
|
-
componentContext.mediaEmit(ms)
|
|
215
|
-
|
|
264
|
+
if (r.componentContext?.mediaEmit) {
|
|
265
|
+
r.componentContext.mediaEmit(ms)
|
|
266
|
+
r.pendingState = ms
|
|
216
267
|
return lastState
|
|
217
268
|
}
|
|
218
269
|
|
|
219
|
-
|
|
270
|
+
r.lastState = ms
|
|
220
271
|
|
|
221
272
|
return ms
|
|
222
273
|
}
|
|
223
274
|
}
|
|
224
275
|
|
|
225
276
|
return lastState
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
277
|
+
}
|
|
278
|
+
internalRef.current = r
|
|
279
|
+
} else {
|
|
280
|
+
// refresh per-render inputs the closures read through the ref
|
|
281
|
+
internalRef.current.componentContext = componentContext
|
|
282
|
+
internalRef.current.debug = debug
|
|
283
|
+
}
|
|
229
284
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
285
|
+
const ref = internalRef.current
|
|
286
|
+
ref.renderVersion++
|
|
287
|
+
|
|
288
|
+
// reset on next render
|
|
289
|
+
if (ref.pendingState) {
|
|
290
|
+
ref.lastState = ref.pendingState
|
|
291
|
+
ref.pendingState = undefined
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// clear each render to track only rendered touched keys
|
|
295
|
+
if (ref.keys.size) {
|
|
296
|
+
ref.keys.clear()
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// manual subscription (same shape as useThemeStateSubscribed): same
|
|
300
|
+
// granular bailout via getSnapshot returning the same MediaQueryState ref
|
|
301
|
+
// when none of the component's touched keys changed, but fewer
|
|
302
|
+
// React-internal hook slots on Hermes than useSyncExternalStore.
|
|
303
|
+
const [, forceUpdate] = useReducer(incReducer, 0)
|
|
304
|
+
const state = isServer ? initState : ref.getSnapshot()
|
|
305
|
+
ref.proxyTarget = state
|
|
306
|
+
;(ref.proxy as any)[refSlot].proxyTarget = state
|
|
307
|
+
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
const renderVersion = ref.renderVersion
|
|
310
|
+
const shouldSubscribe =
|
|
311
|
+
!ref.componentContext || !!States.get(ref.componentContext)?.enabled
|
|
312
|
+
|
|
313
|
+
if (shouldSubscribe) {
|
|
314
|
+
if (!ref.unsubscribe) {
|
|
315
|
+
ref.unsubscribe = subscribe(() => {
|
|
316
|
+
const next = ref.getSnapshot()
|
|
317
|
+
if (next !== ref.proxyTarget) {
|
|
318
|
+
ref.proxyTarget = next
|
|
319
|
+
;(ref.proxy as any)[refSlot].proxyTarget = next
|
|
320
|
+
forceUpdate()
|
|
321
|
+
}
|
|
322
|
+
})
|
|
234
323
|
}
|
|
235
|
-
|
|
236
|
-
|
|
324
|
+
} else if (ref.unsubscribe) {
|
|
325
|
+
ref.unsubscribe()
|
|
326
|
+
ref.unsubscribe = undefined
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return () => {
|
|
330
|
+
// react runs passive cleanup before the next effect as well as on unmount.
|
|
331
|
+
// a newer render bumps renderVersion before that cleanup, so equality here
|
|
332
|
+
// means this is the final unmount cleanup.
|
|
333
|
+
if (ref.renderVersion === renderVersion) {
|
|
334
|
+
ref.unsubscribe?.()
|
|
335
|
+
ref.unsubscribe = undefined
|
|
336
|
+
}
|
|
337
|
+
}
|
|
237
338
|
})
|
|
339
|
+
|
|
340
|
+
return ref.proxy
|
|
238
341
|
}
|
|
239
342
|
|
|
343
|
+
const incReducer = (c: number): number => c + 1
|
|
344
|
+
|
|
240
345
|
const getServerSnapshot = () => initState
|
|
241
346
|
|
|
242
347
|
let disableMediaTouch = false
|
package/src/hooks/useTheme.tsx
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import { useRef } from 'react'
|
|
1
|
+
import { useRef, type MutableRefObject } from 'react'
|
|
2
2
|
import type { ThemeParsed, ThemeState, UseThemeWithStateProps } from '../types'
|
|
3
3
|
import { getThemeProxied, type ThemeProxied } from './getThemeProxied'
|
|
4
4
|
import { useThemeState } from './useThemeState'
|
|
5
5
|
|
|
6
6
|
const EMPTY = {}
|
|
7
7
|
|
|
8
|
+
type KeysBag = {
|
|
9
|
+
keys: MutableRefObject<Set<string> | null>
|
|
10
|
+
schemeKeys: MutableRefObject<Set<string> | null>
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
export const useTheme = () => {
|
|
9
14
|
'use no memo'
|
|
10
15
|
|
|
@@ -20,13 +25,30 @@ export type ThemeWithState = [ThemeParsed, ThemeState]
|
|
|
20
25
|
*/
|
|
21
26
|
export const useThemeWithState = (
|
|
22
27
|
props: UseThemeWithStateProps,
|
|
23
|
-
isRoot = false
|
|
28
|
+
isRoot = false,
|
|
29
|
+
// <Theme> components push their themeState.id into ThemeStateContext,
|
|
30
|
+
// so their descendants subscribe under this id; <Theme> needs to schedule
|
|
31
|
+
// descendant updates when its propsKey changes. Leaf styled components
|
|
32
|
+
// (createComponent, useProps, useTheme, animation hooks) do NOT push a
|
|
33
|
+
// context — listenersByParent[componentId] is always empty — so the
|
|
34
|
+
// schedule-update effect is a no-op for them and can be skipped to save
|
|
35
|
+
// one hook slot per styled component on mount. Theme.tsx passes true.
|
|
36
|
+
forThemeView = false
|
|
24
37
|
): ThemeWithState => {
|
|
25
38
|
'use no memo'
|
|
26
39
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
40
|
+
// single useRef holding both keys + schemeKeys; saves one hook slot per
|
|
41
|
+
// component vs separate useRef calls. these still look like MutableRefObject
|
|
42
|
+
// to getThemeProxied which only reads/writes `.current`.
|
|
43
|
+
const bag = useRef<KeysBag | null>(null)
|
|
44
|
+
if (!bag.current) {
|
|
45
|
+
bag.current = {
|
|
46
|
+
keys: { current: null },
|
|
47
|
+
schemeKeys: { current: null },
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const { keys, schemeKeys } = bag.current
|
|
51
|
+
const themeState = useThemeState(props, isRoot, keys, schemeKeys, forThemeView)
|
|
30
52
|
|
|
31
53
|
if (process.env.NODE_ENV === 'development') {
|
|
32
54
|
if (!props.passThrough && !themeState?.theme) {
|