@carto/ps-react-ui 4.4.3 → 4.5.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.
Files changed (111) hide show
  1. package/dist/{download-config-Dqu78h2a.js → download-config-DemuQ3Jm.js} +9 -10
  2. package/dist/{download-config-Dqu78h2a.js.map → download-config-DemuQ3Jm.js.map} +1 -1
  3. package/dist/error-Cj8eUMrl.js +40 -0
  4. package/dist/error-Cj8eUMrl.js.map +1 -0
  5. package/dist/no-data-DkIt7Qt1.js +61 -0
  6. package/dist/no-data-DkIt7Qt1.js.map +1 -0
  7. package/dist/row-D4VOhcNI.js +34 -0
  8. package/dist/row-D4VOhcNI.js.map +1 -0
  9. package/dist/series-Bola3CmD.js +90 -0
  10. package/dist/series-Bola3CmD.js.map +1 -0
  11. package/dist/types/widgets/echart/shared-resize-observer.d.ts +12 -0
  12. package/dist/types/widgets/stores/index.d.ts +2 -1
  13. package/dist/types/widgets/stores/use-widget-selector.d.ts +35 -0
  14. package/dist/types/widgets/stores/widget-store-performance.test.d.ts +1 -0
  15. package/dist/types/widgets/stores/widget-store.d.ts +49 -27
  16. package/dist/types/widgets/table/types.d.ts +1 -1
  17. package/dist/use-widget-ref-BFazQvJK.js +22 -0
  18. package/dist/use-widget-ref-BFazQvJK.js.map +1 -0
  19. package/dist/use-widget-selector-DqRmWQ1K.js +12 -0
  20. package/dist/use-widget-selector-DqRmWQ1K.js.map +1 -0
  21. package/dist/widget-store-CIrb9RKP.js +263 -0
  22. package/dist/widget-store-CIrb9RKP.js.map +1 -0
  23. package/dist/widgets/actions.js +783 -817
  24. package/dist/widgets/actions.js.map +1 -1
  25. package/dist/widgets/bar.js +2 -2
  26. package/dist/widgets/category.js +254 -257
  27. package/dist/widgets/category.js.map +1 -1
  28. package/dist/widgets/echart.js +109 -99
  29. package/dist/widgets/echart.js.map +1 -1
  30. package/dist/widgets/error.js +1 -1
  31. package/dist/widgets/formula.js +71 -63
  32. package/dist/widgets/formula.js.map +1 -1
  33. package/dist/widgets/histogram.js +7 -8
  34. package/dist/widgets/histogram.js.map +1 -1
  35. package/dist/widgets/loader.js +53 -60
  36. package/dist/widgets/loader.js.map +1 -1
  37. package/dist/widgets/markdown.js +51 -50
  38. package/dist/widgets/markdown.js.map +1 -1
  39. package/dist/widgets/no-data.js +1 -1
  40. package/dist/widgets/pie.js +2 -2
  41. package/dist/widgets/range.js +146 -144
  42. package/dist/widgets/range.js.map +1 -1
  43. package/dist/widgets/scatterplot.js +2 -2
  44. package/dist/widgets/skeleton-loader.js +18 -17
  45. package/dist/widgets/skeleton-loader.js.map +1 -1
  46. package/dist/widgets/spread.js +110 -94
  47. package/dist/widgets/spread.js.map +1 -1
  48. package/dist/widgets/stores.js +5 -2
  49. package/dist/widgets/stores.js.map +1 -1
  50. package/dist/widgets/table.js +422 -436
  51. package/dist/widgets/table.js.map +1 -1
  52. package/dist/widgets/timeseries.js +2 -2
  53. package/dist/widgets/utils.js +1 -1
  54. package/dist/widgets/wrapper.js +156 -158
  55. package/dist/widgets/wrapper.js.map +1 -1
  56. package/dist/widgets.js +4 -4
  57. package/package.json +1 -1
  58. package/src/hooks/use-widget-ref.ts +3 -4
  59. package/src/widgets/actions/brush-toggle/brush-toggle.tsx +18 -32
  60. package/src/widgets/actions/change-column/change-column.tsx +15 -15
  61. package/src/widgets/actions/change-column/sortable-column-item.tsx +3 -1
  62. package/src/widgets/actions/download/download.tsx +4 -3
  63. package/src/widgets/actions/fullscreen/fullscreen.tsx +7 -11
  64. package/src/widgets/actions/lock-selection/lock-selection.tsx +12 -15
  65. package/src/widgets/actions/relative-data/relative-data.tsx +22 -26
  66. package/src/widgets/actions/searcher/searcher-toggle.tsx +11 -12
  67. package/src/widgets/actions/searcher/searcher.tsx +20 -21
  68. package/src/widgets/actions/stack-toggle/stack-toggle.tsx +15 -21
  69. package/src/widgets/actions/zoom-toggle/zoom-toggle.tsx +27 -43
  70. package/src/widgets/category/category-ui.tsx +27 -31
  71. package/src/widgets/echart/echart-ui.test.tsx +20 -16
  72. package/src/widgets/echart/echart-ui.tsx +6 -12
  73. package/src/widgets/echart/echart.tsx +13 -27
  74. package/src/widgets/echart/shared-resize-observer.ts +45 -0
  75. package/src/widgets/error/error.tsx +7 -9
  76. package/src/widgets/formula/components/prefix.tsx +4 -6
  77. package/src/widgets/formula/components/row.tsx +4 -4
  78. package/src/widgets/formula/components/series.tsx +4 -6
  79. package/src/widgets/formula/components/suffix.tsx +4 -6
  80. package/src/widgets/formula/components/value.tsx +9 -16
  81. package/src/widgets/loader/loader.tsx +31 -44
  82. package/src/widgets/markdown/markdown.tsx +4 -7
  83. package/src/widgets/no-data/no-data.tsx +7 -10
  84. package/src/widgets/range/components/range-item.tsx +20 -18
  85. package/src/widgets/skeleton-loader/skeleton-loader.tsx +2 -5
  86. package/src/widgets/spread/components/max-value.tsx +14 -16
  87. package/src/widgets/spread/components/min-value.tsx +14 -16
  88. package/src/widgets/stores/index.ts +2 -1
  89. package/src/widgets/stores/use-widget-selector.ts +47 -0
  90. package/src/widgets/stores/widget-store-performance.test.ts +750 -0
  91. package/src/widgets/stores/widget-store.test.ts +81 -0
  92. package/src/widgets/stores/widget-store.ts +225 -44
  93. package/src/widgets/table/config.ts +0 -1
  94. package/src/widgets/table/hooks/use-pagination.ts +28 -52
  95. package/src/widgets/table/hooks/use-selection.ts +20 -24
  96. package/src/widgets/table/hooks/use-sort.ts +22 -39
  97. package/src/widgets/table/types.ts +1 -1
  98. package/src/widgets/wrapper/wrapper-ui.tsx +12 -13
  99. package/src/widgets/wrapper/wrapper.tsx +4 -6
  100. package/dist/error-CEkRPccv.js +0 -39
  101. package/dist/error-CEkRPccv.js.map +0 -1
  102. package/dist/no-data-hR3KcJ-_.js +0 -60
  103. package/dist/no-data-hR3KcJ-_.js.map +0 -1
  104. package/dist/row-DTCV0Ocm.js +0 -35
  105. package/dist/row-DTCV0Ocm.js.map +0 -1
  106. package/dist/series-CYNOu2Ju.js +0 -91
  107. package/dist/series-CYNOu2Ju.js.map +0 -1
  108. package/dist/use-widget-ref-wtFLDFCD.js +0 -25
  109. package/dist/use-widget-ref-wtFLDFCD.js.map +0 -1
  110. package/dist/widget-store-CzDt8oSK.js +0 -163
  111. package/dist/widget-store-CzDt8oSK.js.map +0 -1
@@ -1,6 +1,6 @@
1
- import { useEffect, useRef, useSyncExternalStore } from 'react'
1
+ import { useEffect, useRef } from 'react'
2
2
  import type { WidgetLoaderProps } from './types'
3
- import { useWidgetStore } from '../stores/widget-store'
3
+ import { useWidgetStore, widgetStoreActions } from '../stores/widget-store'
4
4
  import type { WrapperState } from '../wrapper'
5
5
 
6
6
  /**
@@ -28,17 +28,11 @@ import type { WrapperState } from '../wrapper'
28
28
  export function WidgetLoader<T extends object = Record<string, unknown>>(
29
29
  props: WidgetLoaderProps<T>,
30
30
  ) {
31
- const setWidget = useWidgetStore((state) => state.setWidget)
32
- const executeToolPipeline = useWidgetStore(
33
- (state) => state.executeToolPipeline,
34
- )
35
- const executeConfigPipeline = useWidgetStore(
36
- (state) => state.executeConfigPipeline,
37
- )
38
-
39
- const registeredTools = useSyncExternalStore(
40
- useWidgetStore.subscribe,
41
- () => useWidgetStore.getState().widgets[props.id]?.registeredTools,
31
+ // Subscribe only to this widget's registeredTools via Zustand selector.
32
+ // The selector returns a stable reference when other widgets change,
33
+ // avoiding unnecessary re-evaluations of Effect 4.
34
+ const registeredTools = useWidgetStore(
35
+ (state) => state.widgets[props.id]?.registeredTools,
42
36
  )
43
37
 
44
38
  const dataRef = useRef(props.data)
@@ -50,56 +44,49 @@ export function WidgetLoader<T extends object = Record<string, unknown>>(
50
44
  configRef.current = props.config
51
45
  })
52
46
 
53
- // Split into 3 effects for metadata and 1 for data pipeline:
54
- // Each property that can be modified independently gets its own effect to avoid
55
- // accidentally resetting other properties.
56
- //
57
- // - Effect 1: Type (can be modified by tools that change visualization type)
58
- // - Effect 2: Loading/Error states (change during fetch lifecycle)
59
- // - Effect 3: Config (can be modified by tools that change widget configuration)
60
- // - Effect 4: Data pipeline execution (transforms data through registered tools)
61
- // - Effect 5: Re-execute pipeline when tool state changes
62
-
63
- // Effect 1: Type updates
47
+ // Effect 1: Metadata type, loading, and error states in a single setWidget call.
48
+ // Merged to reduce store updates from 2 to 1 per widget during initialization.
64
49
  useEffect(() => {
65
- setWidget<WrapperState>(props.id, {
50
+ widgetStoreActions.setWidget<WrapperState>(props.id, {
66
51
  type: props.type,
67
- })
68
- }, [props.id, props.type, setWidget])
69
-
70
- // Effect 2: Loading and error states
71
- useEffect(() => {
72
- setWidget<WrapperState>(props.id, {
73
52
  isLoading: props.isLoading ?? false,
74
53
  isFetching: props.isFetching ?? false,
75
54
  error: props.error,
76
55
  })
77
- }, [props.id, props.isLoading, props.isFetching, props.error, setWidget])
56
+ }, [props.id, props.type, props.isLoading, props.isFetching, props.error])
78
57
 
79
- // Effect 3: Config updates — run through config pipeline
58
+ // Effect 2: Config updates — run through config pipeline
80
59
  useEffect(() => {
81
60
  if (props.config) {
82
- void executeConfigPipeline(props.id, props.config)
61
+ void widgetStoreActions.executeConfigPipeline(props.id, props.config)
83
62
  }
84
- }, [props.id, props.config, executeConfigPipeline])
63
+ }, [props.id, props.config])
85
64
 
86
- // Effect 4: Execute tool pipeline when props.data changes
65
+ // Effect 3: Execute tool pipeline when props.data changes
87
66
  useEffect(() => {
88
- void executeToolPipeline(props.id, props.data)
89
- }, [props.id, props.data, executeToolPipeline])
67
+ void widgetStoreActions.executeToolPipeline(props.id, props.data)
68
+ }, [props.id, props.data])
90
69
 
91
- // Effect 5: Re-execute pipelines when registered tools change
70
+ // Effect 4: Re-execute pipelines when registered tools change.
71
+ // Uses requestAnimationFrame to coalesce rapid successive registeredTools
72
+ // changes (e.g., 6 action components each calling registerTool on mount)
73
+ // into a single pipeline execution instead of 6 pairs.
92
74
  useEffect(() => {
93
75
  if (!isMountedRef.current) {
94
76
  isMountedRef.current = true
95
77
  return
96
78
  }
97
79
 
98
- void executeToolPipeline(props.id, dataRef.current)
99
- if (configRef.current) {
100
- void executeConfigPipeline(props.id, configRef.current)
101
- }
102
- }, [registeredTools, props.id, executeToolPipeline, executeConfigPipeline])
80
+ const rafId = requestAnimationFrame(() => {
81
+ const { executeToolPipeline, executeConfigPipeline } = widgetStoreActions
82
+ void executeToolPipeline(props.id, dataRef.current)
83
+ if (configRef.current) {
84
+ void executeConfigPipeline(props.id, configRef.current)
85
+ }
86
+ })
87
+
88
+ return () => cancelAnimationFrame(rafId)
89
+ }, [registeredTools, props.id])
103
90
 
104
91
  return props.children
105
92
  }
@@ -1,18 +1,15 @@
1
1
  import type { MarkdownUIProps } from './types'
2
- import { useWidgetStore } from '../stores/widget-store'
3
2
  import type { MarkdownWidgetData } from './types'
4
3
  import { MarkdownUI } from '.'
5
- import { useShallow } from 'zustand/shallow'
4
+ import { useWidgetSelector } from '../stores/use-widget-selector'
6
5
 
7
6
  /**
8
7
  * Stateful markdown widget component that reads content from the widget store and renders it as formatted rich text.
9
8
  */
10
9
  export function Markdown({ id }: MarkdownUIProps) {
11
- const content = useWidgetStore(
12
- useShallow(
13
- (state) =>
14
- (state.getWidget(id)?.data as MarkdownWidgetData | undefined)?.content,
15
- ),
10
+ const content = useWidgetSelector(
11
+ id,
12
+ (w) => (w?.data as MarkdownWidgetData | undefined)?.content,
16
13
  )
17
14
 
18
15
  if (!content) {
@@ -1,6 +1,5 @@
1
1
  import { Box, Typography } from '@mui/material'
2
- import { useShallow } from 'zustand/shallow'
3
- import { useWidgetStore } from '../stores/widget-store'
2
+ import { useWidgetSelector } from '../stores/use-widget-selector'
4
3
  import type { WidgetNoDataProps } from './types'
5
4
  import { styles } from './style'
6
5
 
@@ -44,14 +43,12 @@ export function WidgetNoData({
44
43
  description = 'There are no results for the combination of filters applied to your data. Try tweaking your filters, or zoom and pan the map to adjust filters',
45
44
  isEmpty = defaultIsEmpty,
46
45
  }: WidgetNoDataProps) {
47
- // Subscribe to widget store with selective subscription for optimal performance
48
- const isLoading = useWidgetStore(
49
- useShallow((state) => state.widgets[id]?.isLoading),
50
- )
51
- const isFetching = useWidgetStore(
52
- useShallow((state) => state.widgets[id]?.isFetching),
53
- )
54
- const data = useWidgetStore(useShallow((state) => state.widgets[id]?.data))
46
+ // Single consolidated subscription instead of 3 separate ones.
47
+ const { isLoading, isFetching, data } = useWidgetSelector(id, (w) => ({
48
+ isLoading: w?.isLoading,
49
+ isFetching: w?.isFetching,
50
+ data: w?.data,
51
+ }))
55
52
 
56
53
  // If loading or fetching, show children
57
54
  // SkeletonLoader handles loading state, this allows proper composition
@@ -1,9 +1,9 @@
1
1
  import { Box, Slider, TextField } from '@mui/material'
2
2
  import { useState, useMemo, type FocusEvent, type KeyboardEvent } from 'react'
3
- import { useWidgetStore } from '../../stores/widget-store'
3
+ import { widgetStoreActions } from '../../stores/widget-store'
4
+ import { useWidgetSelector } from '../../stores/use-widget-selector'
4
5
  import type { RangeItemProps, RangeWidgetState } from '../types'
5
6
  import { styles } from '../style'
6
- import { useShallow } from 'zustand/shallow'
7
7
 
8
8
  import { defaultFormatter } from '../../utils/formatter'
9
9
 
@@ -13,18 +13,19 @@ type EditingState = '' | 'min' | 'max'
13
13
  * Renders a single range slider with editable min/max text inputs, reading its configuration from the widget store.
14
14
  */
15
15
  export function RangeItem({ id, index }: RangeItemProps) {
16
- const item = useWidgetStore(
17
- useShallow((state) => state.getWidget<RangeWidgetState>(id)?.data[index]),
18
- )
19
- const onChange = useWidgetStore(
20
- useShallow((state) => state.getWidget<RangeWidgetState>(id)?.onChange),
21
- )
22
- const formatter =
23
- useWidgetStore(
24
- useShallow((state) => state.getWidget<RangeWidgetState>(id)?.formatter),
25
- ) ?? defaultFormatter
26
- const getWidget = useWidgetStore((store) => store.getWidget)
27
- const setWidget = useWidgetStore((store) => store.setWidget)
16
+ const {
17
+ item,
18
+ onChange,
19
+ formatter: _formatter,
20
+ } = useWidgetSelector(id, (w) => {
21
+ const rw = w as RangeWidgetState | undefined
22
+ return {
23
+ item: rw?.data[index],
24
+ onChange: rw?.onChange,
25
+ formatter: rw?.formatter,
26
+ }
27
+ })
28
+ const formatter = _formatter ?? defaultFormatter
28
29
 
29
30
  const currentValue = useMemo(
30
31
  () => (item ? (item.value ?? [item.min, item.max]) : [0, 0]),
@@ -39,14 +40,15 @@ export function RangeItem({ id, index }: RangeItemProps) {
39
40
  const handleSliderChange = (_: Event, newValue: number | number[]) => {
40
41
  if (Array.isArray(newValue)) {
41
42
  const [min, max] = newValue
42
- const data = getWidget<RangeWidgetState>(id)?.data ?? []
43
+ const data =
44
+ widgetStoreActions.getWidget<RangeWidgetState>(id)?.data ?? []
43
45
 
44
46
  data[index] = {
45
47
  ...item,
46
48
  value: newValue,
47
49
  }
48
50
 
49
- setWidget(id, {
51
+ widgetStoreActions.setWidget(id, {
50
52
  data,
51
53
  })
52
54
 
@@ -80,9 +82,9 @@ export function RangeItem({ id, index }: RangeItemProps) {
80
82
  ]
81
83
  }
82
84
 
83
- const data = getWidget<RangeWidgetState>(id)?.data ?? []
85
+ const data = widgetStoreActions.getWidget<RangeWidgetState>(id)?.data ?? []
84
86
 
85
- setWidget(id, {
87
+ widgetStoreActions.setWidget(id, {
86
88
  data: data.map((d: RangeWidgetState['data'][number], i: number) =>
87
89
  i === index
88
90
  ? {
@@ -1,7 +1,6 @@
1
1
  import type { SkeletonLoaderProps } from './types'
2
- import { useWidgetStore } from '../stores/widget-store'
2
+ import { useWidgetSelector } from '../stores/use-widget-selector'
3
3
  import { Suspense } from 'react'
4
- import { useShallow } from 'zustand/shallow'
5
4
 
6
5
  /**
7
6
  * Displays a skeleton loading placeholder while widget data is loading. Subscribes to widget loading state in the store and renders the provided Skeleton component or children accordingly.
@@ -18,9 +17,7 @@ export function SkeletonLoader({
18
17
  children,
19
18
  Skeleton,
20
19
  }: SkeletonLoaderProps) {
21
- const isLoading = useWidgetStore(
22
- useShallow((state) => state.widgets[id]?.isLoading),
23
- )
20
+ const isLoading = useWidgetSelector(id, (w) => w?.isLoading)
24
21
 
25
22
  if (isLoading) {
26
23
  if (!Skeleton) {
@@ -1,27 +1,25 @@
1
1
  import { type SpreadWidgetState, type ValueProps } from '../types'
2
- import { useWidgetStore } from '../../stores/widget-store'
2
+ import { useWidgetSelector } from '../../stores/use-widget-selector'
3
3
  import { Item } from '../../formula/components/item'
4
- import { useShallow } from 'zustand/shallow'
5
4
  import { defaultFormatter } from '../../utils/formatter'
6
5
 
7
6
  /**
8
7
  * Displays the formatted maximum value for a spread widget data item.
9
8
  */
10
9
  export function MaxValue({ id, index = 0, ...props }: ValueProps) {
11
- const max = useWidgetStore(
12
- useShallow(
13
- (state) => state.getWidget<SpreadWidgetState>(id)?.data[index]?.max,
14
- ),
15
- )
16
- const color = useWidgetStore(
17
- useShallow(
18
- (state) => state.getWidget<SpreadWidgetState>(id)?.data[index]?.color,
19
- ),
20
- )
21
- const formatter =
22
- useWidgetStore(
23
- useShallow((state) => state.getWidget<SpreadWidgetState>(id)?.formatter),
24
- ) ?? defaultFormatter
10
+ const {
11
+ max,
12
+ color,
13
+ formatter: _formatter,
14
+ } = useWidgetSelector(id, (w) => {
15
+ const sw = w as SpreadWidgetState | undefined
16
+ return {
17
+ max: sw?.data[index]?.max,
18
+ color: sw?.data[index]?.color,
19
+ formatter: sw?.formatter,
20
+ }
21
+ })
22
+ const formatter = _formatter ?? defaultFormatter
25
23
 
26
24
  return (
27
25
  <Item TypographyProps={{ color }} {...props}>
@@ -1,27 +1,25 @@
1
1
  import { type SpreadWidgetState, type ValueProps } from '../types'
2
- import { useWidgetStore } from '../../stores/widget-store'
2
+ import { useWidgetSelector } from '../../stores/use-widget-selector'
3
3
  import { Item } from '../../formula/components/item'
4
- import { useShallow } from 'zustand/shallow'
5
4
  import { defaultFormatter } from '../../utils/formatter'
6
5
 
7
6
  /**
8
7
  * Displays the formatted minimum value for a spread widget data item.
9
8
  */
10
9
  export function MinValue({ id, index = 0, ...props }: ValueProps) {
11
- const min = useWidgetStore(
12
- useShallow(
13
- (state) => state.getWidget<SpreadWidgetState>(id)?.data[index]?.min,
14
- ),
15
- )
16
- const color = useWidgetStore(
17
- useShallow(
18
- (state) => state.getWidget<SpreadWidgetState>(id)?.data[index]?.color,
19
- ),
20
- )
21
- const formatter =
22
- useWidgetStore(
23
- useShallow((state) => state.getWidget<SpreadWidgetState>(id)?.formatter),
24
- ) ?? defaultFormatter
10
+ const {
11
+ min,
12
+ color,
13
+ formatter: _formatter,
14
+ } = useWidgetSelector(id, (w) => {
15
+ const sw = w as SpreadWidgetState | undefined
16
+ return {
17
+ min: sw?.data[index]?.min,
18
+ color: sw?.data[index]?.color,
19
+ formatter: sw?.formatter,
20
+ }
21
+ })
22
+ const formatter = _formatter ?? defaultFormatter
25
23
 
26
24
  return (
27
25
  <Item TypographyProps={{ color }} {...props}>
@@ -1,4 +1,5 @@
1
- export { useWidgetStore } from './widget-store'
1
+ export { useWidgetStore, widgetStoreActions } from './widget-store'
2
+ export { useWidgetSelector } from './use-widget-selector'
2
3
  export type {
3
4
  BaseWidgetState,
4
5
  ToolType,
@@ -0,0 +1,47 @@
1
+ import { useWidgetStore } from './widget-store'
2
+ import type { WidgetState } from './types'
3
+ import { useShallow } from 'zustand/shallow'
4
+
5
+ /**
6
+ * Scoped selector hook for reading a single widget's state from the store.
7
+ *
8
+ * Consolidates multiple `useWidgetStore(useShallow(...))` calls into a single
9
+ * subscription per component. The selector receives only this widget's state
10
+ * (or undefined if not yet registered), and uses shallow comparison to avoid
11
+ * re-renders when unrelated properties change.
12
+ *
13
+ * @param widgetId - The widget ID to subscribe to.
14
+ * @param selector - A function that extracts the needed properties from the widget state.
15
+ * Must be a stable reference (inline arrow is fine due to useCallback wrapping).
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * // Before: 4 separate subscriptions
20
+ * const title = useWidgetStore(useShallow((s) => s.getWidget(id)?.title))
21
+ * const collapsed = useWidgetStore(useShallow((s) => s.getWidget(id)?.collapsed))
22
+ * const disabled = useWidgetStore(useShallow((s) => s.getWidget(id)?.disabled))
23
+ * const isFetching = useWidgetStore(useShallow((s) => s.getWidget(id)?.isFetching))
24
+ *
25
+ * // After: 1 subscription
26
+ * const { title, collapsed, disabled, isFetching } = useWidgetSelector(id, (w) => ({
27
+ * title: w?.title, collapsed: w?.collapsed, disabled: w?.disabled, isFetching: w?.isFetching,
28
+ * }))
29
+ *
30
+ * // With extra dependencies (e.g., index prop):
31
+ * const value = useWidgetSelector(
32
+ * id,
33
+ * (w) => (w as MyState | undefined)?.data?.[index]?.value,
34
+ * [index],
35
+ * )
36
+ * ```
37
+ */
38
+ export function useWidgetSelector<T>(
39
+ widgetId: string,
40
+ selector: (widget: WidgetState | undefined) => T,
41
+ ): T {
42
+ return useWidgetStore(
43
+ useShallow((state: { widgets: Record<string, WidgetState> }) =>
44
+ selector(state.widgets[widgetId]),
45
+ ),
46
+ )
47
+ }