diff --git a/packages/x-charts-pro/src/FunnelChart/funnelAxisPlugin/useChartFunnelAxis.ts b/packages/x-charts-pro/src/FunnelChart/funnelAxisPlugin/useChartFunnelAxis.ts index 2390602b4773c..a5792dd1cc951 100644 --- a/packages/x-charts-pro/src/FunnelChart/funnelAxisPlugin/useChartFunnelAxis.ts +++ b/packages/x-charts-pro/src/FunnelChart/funnelAxisPlugin/useChartFunnelAxis.ts @@ -52,17 +52,15 @@ export const useChartFunnelAxis: ChartPlugin = ({ return; } - store.update((prev) => ({ - ...prev, + store.update({ funnel: { gap: gap ?? 0, }, cartesianAxis: { - ...prev.cartesianAxis, x: defaultizeXAxis(xAxis, dataset), y: defaultizeYAxis(yAxis, dataset), }, - })); + }); }, [seriesConfig, drawingArea, xAxis, yAxis, dataset, store, gap]); React.useEffect(() => { @@ -129,9 +127,9 @@ export const useChartFunnelAxis: ChartPlugin = ({ } const axisClickHandler = instance.addInteractionListener('tap', (event) => { - const { axis: xAxisWithScale, axisIds: xAxisIds } = selectorChartXAxis(store.value); - const { axis: yAxisWithScale, axisIds: yAxisIds } = selectorChartYAxis(store.value); - const processedSeries = selectorChartSeriesProcessed(store.value); + const { axis: xAxisWithScale, axisIds: xAxisIds } = selectorChartXAxis(store.state); + const { axis: yAxisWithScale, axisIds: yAxisIds } = selectorChartYAxis(store.state); + const processedSeries = selectorChartSeriesProcessed(store.state); const usedXAxis = xAxisIds[0]; const usedYAxis = yAxisIds[0]; diff --git a/packages/x-charts-pro/src/SankeyChart/plugins/useSankeyHighlight.ts b/packages/x-charts-pro/src/SankeyChart/plugins/useSankeyHighlight.ts index b608cbae8d7cf..8ff5c3b43b72e 100644 --- a/packages/x-charts-pro/src/SankeyChart/plugins/useSankeyHighlight.ts +++ b/packages/x-charts-pro/src/SankeyChart/plugins/useSankeyHighlight.ts @@ -22,22 +22,14 @@ export const useSankeyHighlight: ChartPlugin = ({ s }); useEnhancedEffect(() => { - store.update((prevState) => - prevState.highlight.item === params.highlightedItem - ? prevState - : { - ...prevState, - highlight: { - ...prevState.highlight, - item: params.highlightedItem, - }, - }, - ); + if (store.state.highlight.item !== params.highlightedItem) { + store.set('highlight', { ...store.state.highlight, item: params.highlightedItem }); + } }, [store, params.highlightedItem]); const clearHighlight = useEventCallback(() => { params.onHighlightChange?.(null); - store.update((prev) => ({ ...prev, highlight: { item: null } })); + store.set('highlight', { ...store.state.highlight, item: null }); }); const setHighlight = useEventCallback((newItem: SankeyHighlightItemData) => { @@ -48,7 +40,7 @@ export const useSankeyHighlight: ChartPlugin = ({ s } params.onHighlightChange?.(newItem); - store.update((prev) => ({ ...prev, highlight: { item: newItem } })); + store.set('highlight', { ...store.state.highlight, item: newItem }); }); return { diff --git a/packages/x-charts-pro/src/internals/plugins/useChartProZoom/gestureHooks/usePanOnDrag.ts b/packages/x-charts-pro/src/internals/plugins/useChartProZoom/gestureHooks/usePanOnDrag.ts index ac400fab2179f..92ca8d5f533f8 100644 --- a/packages/x-charts-pro/src/internals/plugins/useChartProZoom/gestureHooks/usePanOnDrag.ts +++ b/packages/x-charts-pro/src/internals/plugins/useChartProZoom/gestureHooks/usePanOnDrag.ts @@ -54,7 +54,7 @@ export const usePanOnDrag = ( const handlePanStart = (event: PanEvent) => { if (!(event.detail.target as SVGElement)?.closest('[data-charts-zoom-slider]')) { - startRef.current = store.value.zoom.zoomData; + startRef.current = store.state.zoom.zoomData; } }; const handlePanEnd = () => { diff --git a/packages/x-charts-pro/src/internals/plugins/useChartProZoom/gestureHooks/usePanOnPressAndDrag.ts b/packages/x-charts-pro/src/internals/plugins/useChartProZoom/gestureHooks/usePanOnPressAndDrag.ts index 669692c3823ab..f32271488cfc1 100644 --- a/packages/x-charts-pro/src/internals/plugins/useChartProZoom/gestureHooks/usePanOnPressAndDrag.ts +++ b/packages/x-charts-pro/src/internals/plugins/useChartProZoom/gestureHooks/usePanOnPressAndDrag.ts @@ -54,7 +54,7 @@ export const usePanOnPressAndDrag = ( const handlePressAndDragStart = (event: PressAndDragEvent) => { if (!(event.detail.target as SVGElement)?.closest('[data-charts-zoom-slider]')) { - startRef.current = store.value.zoom.zoomData; + startRef.current = store.state.zoom.zoomData; } }; diff --git a/packages/x-charts-pro/src/internals/plugins/useChartProZoom/useChartProZoom.ts b/packages/x-charts-pro/src/internals/plugins/useChartProZoom/useChartProZoom.ts index cf1403ed4057a..d6174673eb8a6 100644 --- a/packages/x-charts-pro/src/internals/plugins/useChartProZoom/useChartProZoom.ts +++ b/packages/x-charts-pro/src/internals/plugins/useChartProZoom/useChartProZoom.ts @@ -36,14 +36,9 @@ export const useChartProZoom: ChartPlugin = (pluginDat const optionsLookup = useSelector(store, selectorChartZoomOptionsLookup); useEffectAfterFirstRender(() => { - store.update((prevState) => { - return { - ...prevState, - zoom: { - ...prevState.zoom, - zoomInteractionConfig: initializeZoomInteractionConfig(zoomInteractionConfig), - }, - }; + store.set('zoom', { + ...store.state.zoom, + zoomInteractionConfig: initializeZoomInteractionConfig(zoomInteractionConfig), }); }, [store, zoomInteractionConfig]); @@ -52,38 +47,28 @@ export const useChartProZoom: ChartPlugin = (pluginDat if (paramsZoomData === undefined) { return undefined; } - store.update((prevState) => { - if (process.env.NODE_ENV !== 'production' && !prevState.zoom.isControlled) { - console.error( - [ - `MUI X Charts: A chart component is changing the \`zoomData\` from uncontrolled to controlled.`, - 'Elements should not switch from uncontrolled to controlled (or vice versa).', - 'Decide between using a controlled or uncontrolled for the lifetime of the component.', - "The nature of the state is determined during the first render. It's considered controlled if the value is not `undefined`.", - 'More info: https://fb.me/react-controlled-components', - ].join('\n'), - ); - } - return { - ...prevState, - zoom: { - ...prevState.zoom, - isInteracting: true, - zoomData: paramsZoomData, - }, - }; + if (process.env.NODE_ENV !== 'production' && !store.state.zoom.isControlled) { + console.error( + [ + `MUI X Charts: A chart component is changing the \`zoomData\` from uncontrolled to controlled.`, + 'Elements should not switch from uncontrolled to controlled (or vice versa).', + 'Decide between using a controlled or uncontrolled for the lifetime of the component.', + "The nature of the state is determined during the first render. It's considered controlled if the value is not `undefined`.", + 'More info: https://fb.me/react-controlled-components', + ].join('\n'), + ); + } + store.set('zoom', { + ...store.state.zoom, + isInteracting: true, + zoomData: paramsZoomData, }); const timeout = setTimeout(() => { - store.update((prevState) => { - return { - ...prevState, - zoom: { - ...prevState.zoom, - isInteracting: false, - }, - }; + store.set('zoom', { + ...store.state.zoom, + isInteracting: false, }); }, 166); @@ -97,14 +82,9 @@ export const useChartProZoom: ChartPlugin = (pluginDat () => debounce( () => - store.update((prevState) => { - return { - ...prevState, - zoom: { - ...prevState.zoom, - isInteracting: false, - }, - }; + store.set('zoom', { + ...store.state.zoom, + isInteracting: false, }), 166, ), @@ -113,23 +93,17 @@ export const useChartProZoom: ChartPlugin = (pluginDat const setZoomDataCallback = React.useCallback( (zoomData: ZoomData[] | ((prev: ZoomData[]) => ZoomData[])) => { - store.update((prevState) => { - const newZoomData = - typeof zoomData === 'function' ? zoomData([...prevState.zoom.zoomData]) : zoomData; - onZoomChange?.(newZoomData); - if (prevState.zoom.isControlled) { - return prevState; - } - - removeIsInteracting(); - return { - ...prevState, - zoom: { - ...prevState.zoom, - isInteracting: true, - zoomData: newZoomData, - }, - }; + const newZoomData = + typeof zoomData === 'function' ? zoomData([...store.state.zoom.zoomData]) : zoomData; + onZoomChange?.(newZoomData); + if (store.state.zoom.isControlled) { + return; + } + removeIsInteracting(); + store.set('zoom', { + ...store.state.zoom, + isInteracting: true, + zoomData: newZoomData, }); }, [onZoomChange, store, removeIsInteracting], diff --git a/packages/x-charts/src/context/ChartProvider/ChartProvider.types.ts b/packages/x-charts/src/context/ChartProvider/ChartProvider.types.ts index 180366f48fa0b..a340937e0a86e 100644 --- a/packages/x-charts/src/context/ChartProvider/ChartProvider.types.ts +++ b/packages/x-charts/src/context/ChartProvider/ChartProvider.types.ts @@ -1,12 +1,13 @@ import * as React from 'react'; +import { Store } from '@mui/x-internals/store'; import type { ChartAnyPluginSignature, ChartInstance, ChartPublicAPI, + ChartState, ConvertSignaturesIntoPlugins, MergeSignaturesProperty, } from '../../internals/plugins/models'; -import type { ChartStore } from '../../internals/plugins/utils/ChartStore'; import type { ChartCorePluginSignatures } from '../../internals/plugins/corePlugins'; import type { ChartSeriesConfig } from '../../internals/plugins/models/seriesConfig'; import type { UseChartBaseProps } from '../../internals/store/useCharts.types'; @@ -27,7 +28,7 @@ export type ChartContextValue< /** * The internal state of the chart. */ - store: ChartStore; + store: Store>; /** * The ref to the . */ diff --git a/packages/x-charts/src/hooks/useItemHighlightedGetter.tsx b/packages/x-charts/src/hooks/useItemHighlightedGetter.tsx index 715493085753b..077e255005b62 100644 --- a/packages/x-charts/src/hooks/useItemHighlightedGetter.tsx +++ b/packages/x-charts/src/hooks/useItemHighlightedGetter.tsx @@ -5,6 +5,7 @@ import { selectorChartsIsFadedCallback, selectorChartsIsHighlightedCallback, } from '../internals/plugins/featurePlugins/useChartHighlight/useChartHighlight.selectors'; +import { UseChartHighlightSignature } from '../plugins'; /** * A hook to check the highlighted state of multiple items. @@ -15,7 +16,7 @@ import { * @returns {{ isHighlighted, isFaded }} callbacks to get the state of the item. */ export function useItemHighlightedGetter() { - const store = useStore(); + const store = useStore<[UseChartHighlightSignature]>(); const isHighlighted = useSelector(store, selectorChartsIsHighlightedCallback); const isFaded = useSelector(store, selectorChartsIsFadedCallback); diff --git a/packages/x-charts/src/internals/plugins/corePlugins/useChartAnimation/useChartAnimation.ts b/packages/x-charts/src/internals/plugins/corePlugins/useChartAnimation/useChartAnimation.ts index 130f9f3feeeec..9af952b98aee4 100644 --- a/packages/x-charts/src/internals/plugins/corePlugins/useChartAnimation/useChartAnimation.ts +++ b/packages/x-charts/src/internals/plugins/corePlugins/useChartAnimation/useChartAnimation.ts @@ -6,24 +6,16 @@ import type { UseChartAnimationSignature } from './useChartAnimation.types'; export const useChartAnimation: ChartPlugin = ({ params, store }) => { React.useEffect(() => { - store.update((prevState) => { - return { - ...prevState, - animation: { ...prevState.animation, skip: params.skipAnimation }, - }; - }); + store.set('animation', { ...store.state.animation, skip: params.skipAnimation }); }, [store, params.skipAnimation]); const disableAnimation = React.useCallback(() => { let disableCalled = false; - store.update((prevState) => ({ - ...prevState, - animation: { - ...prevState.animation, - skipAnimationRequests: prevState.animation.skipAnimationRequests + 1, - }, - })); + store.set('animation', { + ...store.state.animation, + skipAnimationRequests: store.state.animation.skipAnimationRequests + 1, + }); return () => { if (disableCalled) { @@ -32,13 +24,10 @@ export const useChartAnimation: ChartPlugin = ({ par disableCalled = true; - store.update((prevState) => ({ - ...prevState, - animation: { - ...prevState.animation, - skipAnimationRequests: prevState.animation.skipAnimationRequests - 1, - }, - })); + store.set('animation', { + ...store.state.animation, + skipAnimationRequests: store.state.animation.skipAnimationRequests - 1, + }); }; }, [store]); diff --git a/packages/x-charts/src/internals/plugins/corePlugins/useChartDimensions/useChartDimensions.ts b/packages/x-charts/src/internals/plugins/corePlugins/useChartDimensions/useChartDimensions.ts index ca218ca3aeae5..3b17f0d66c8aa 100644 --- a/packages/x-charts/src/internals/plugins/corePlugins/useChartDimensions/useChartDimensions.ts +++ b/packages/x-charts/src/internals/plugins/corePlugins/useChartDimensions/useChartDimensions.ts @@ -1,5 +1,6 @@ 'use client'; import * as React from 'react'; +import { useEffectAfterFirstRender } from '@mui/x-internals/useEffectAfterFirstRender'; import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; import ownerWindow from '@mui/utils/ownerWindow'; import { useSelector } from '../../../store/useSelector'; @@ -35,27 +36,20 @@ export const useChartDimensions: ChartPlugin = ({ const newHeight = Math.floor(parseFloat(computedStyle.height)) || 0; const newWidth = Math.floor(parseFloat(computedStyle.width)) || 0; - store.update((prev) => { - if (prev.dimensions.width === newWidth && prev.dimensions.height === newHeight) { - return prev; - } - - return { - ...prev, - dimensions: { - margin: { - top: params.margin.top, - right: params.margin.right, - bottom: params.margin.bottom, - left: params.margin.left, - }, - width: params.width ?? newWidth, - height: params.height ?? newHeight, - propsWidth: params.width, - propsHeight: params.height, + if (store.state.dimensions.width !== newWidth || store.state.dimensions.height !== newHeight) { + store.set('dimensions', { + margin: { + top: params.margin.top, + right: params.margin.right, + bottom: params.margin.bottom, + left: params.margin.left, }, - }; - }); + width: params.width ?? newWidth, + height: params.height ?? newHeight, + propsWidth: params.width, + propsHeight: params.height, + }); + } return { height: newHeight, width: newWidth, @@ -72,26 +66,20 @@ export const useChartDimensions: ChartPlugin = ({ params.margin.bottom, ]); - React.useEffect(() => { - store.update((prev) => { - const width = params.width ?? prev.dimensions.width; - const height = params.height ?? prev.dimensions.height; - - return { - ...prev, - dimensions: { - margin: { - top: params.margin.top, - right: params.margin.right, - bottom: params.margin.bottom, - left: params.margin.left, - }, - width, - height, - propsHeight: params.height, - propsWidth: params.width, - }, - }; + useEffectAfterFirstRender(() => { + const width = params.width ?? store.state.dimensions.width; + const height = params.height ?? store.state.dimensions.height; + store.set('dimensions', { + margin: { + top: params.margin.top, + right: params.margin.right, + bottom: params.margin.bottom, + left: params.margin.left, + }, + width, + height, + propsHeight: params.height, + propsWidth: params.width, }); }, [ store, diff --git a/packages/x-charts/src/internals/plugins/corePlugins/useChartExperimentalFeature/useChartExperimentalFeature.ts b/packages/x-charts/src/internals/plugins/corePlugins/useChartExperimentalFeature/useChartExperimentalFeature.ts index eeed7dbc9407e..028a1c88575cf 100644 --- a/packages/x-charts/src/internals/plugins/corePlugins/useChartExperimentalFeature/useChartExperimentalFeature.ts +++ b/packages/x-charts/src/internals/plugins/corePlugins/useChartExperimentalFeature/useChartExperimentalFeature.ts @@ -8,12 +8,7 @@ export const useChartExperimentalFeatures: ChartPlugin { useEnhancedEffect(() => { - store.update((prevState) => { - return { - ...prevState, - experimentalFeatures: params.experimentalFeatures, - }; - }); + store.set('experimentalFeatures', params.experimentalFeatures); }, [store, params.experimentalFeatures]); return {}; diff --git a/packages/x-charts/src/internals/plugins/corePlugins/useChartId/useChartId.ts b/packages/x-charts/src/internals/plugins/corePlugins/useChartId/useChartId.ts index e30d3568960cf..b1ba5241cb146 100644 --- a/packages/x-charts/src/internals/plugins/corePlugins/useChartId/useChartId.ts +++ b/packages/x-charts/src/internals/plugins/corePlugins/useChartId/useChartId.ts @@ -6,19 +6,13 @@ import { createChartDefaultId } from './useChartId.utils'; export const useChartId: ChartPlugin = ({ params, store }) => { React.useEffect(() => { - store.update((prevState) => { - if ( - params.id === undefined || - (params.id === prevState.id.providedChartId && prevState.id.chartId !== undefined) - ) { - return prevState; - } - - return { - ...prevState, - id: { ...prevState.id, chartId: params.id ?? createChartDefaultId() }, - }; - }); + if ( + params.id === undefined || + (params.id === store.state.id.providedChartId && store.state.id.chartId !== undefined) + ) { + return; + } + store.set('id', { ...store.state.id, chartId: params.id ?? createChartDefaultId() }); }, [store, params.id]); return {}; }; diff --git a/packages/x-charts/src/internals/plugins/corePlugins/useChartSeries/useChartSeries.ts b/packages/x-charts/src/internals/plugins/corePlugins/useChartSeries/useChartSeries.ts index 841be15dc2fa4..247502525799b 100644 --- a/packages/x-charts/src/internals/plugins/corePlugins/useChartSeries/useChartSeries.ts +++ b/packages/x-charts/src/internals/plugins/corePlugins/useChartSeries/useChartSeries.ts @@ -22,18 +22,15 @@ export const useChartSeries: ChartPlugin = ({ return; } - store.update((prev) => ({ - ...prev, - series: { - ...prev.series, - processedSeries: preprocessSeries({ - series, - colors: typeof colors === 'function' ? colors(theme) : colors, - seriesConfig, - dataset, - }), - }, - })); + store.set('series', { + ...store.state.series, + processedSeries: preprocessSeries({ + series, + colors: typeof colors === 'function' ? colors(theme) : colors, + seriesConfig, + dataset, + }), + }); }, [colors, dataset, series, theme, seriesConfig, store]); return {}; diff --git a/packages/x-charts/src/internals/plugins/featurePlugins/useChartBrush/useChartBrush.ts b/packages/x-charts/src/internals/plugins/featurePlugins/useChartBrush/useChartBrush.ts index cdfbaa316138c..0de5e34b97c3f 100644 --- a/packages/x-charts/src/internals/plugins/featurePlugins/useChartBrush/useChartBrush.ts +++ b/packages/x-charts/src/internals/plugins/featurePlugins/useChartBrush/useChartBrush.ts @@ -18,16 +18,11 @@ export const useChartBrush: ChartPlugin = ({ const isEnabled = useSelector(store, selectorIsBrushEnabled); useEnhancedEffect(() => { - store.update((prev) => { - return { - ...prev, - brush: { - ...prev.brush, - enabled: params.brushConfig.enabled, - preventTooltip: params.brushConfig.preventTooltip, - preventHighlight: params.brushConfig.preventHighlight, - }, - }; + store.set('brush', { + ...store.state.brush, + enabled: params.brushConfig.enabled, + preventTooltip: params.brushConfig.preventTooltip, + preventHighlight: params.brushConfig.preventHighlight, }); }, [ store, @@ -37,44 +32,28 @@ export const useChartBrush: ChartPlugin = ({ ]); const setBrushCoordinates = useEventCallback(function setBrushCoordinates(point: Point | null) { - store.update((prev) => { - return { - ...prev, - brush: { - ...prev.brush, - start: prev.brush.start ?? point, - current: point, - }, - }; + store.set('brush', { + ...store.state.brush, + start: store.state.brush.start ?? point, + current: point, }); }); const clearBrush = useEventCallback(function clearBrush() { - store.update((prev) => { - return { - ...prev, - brush: { - ...prev.brush, - start: null, - current: null, - }, - }; + store.set('brush', { + ...store.state.brush, + start: null, + current: null, }); }); const setZoomBrushEnabled = useEventCallback(function setZoomBrushEnabled(enabled: boolean) { - store.update((prev) => { - if (prev.brush.isZoomBrushEnabled === enabled) { - return prev; - } - - return { - ...prev, - brush: { - ...prev.brush, - isZoomBrushEnabled: enabled, - }, - }; + if (store.state.brush.isZoomBrushEnabled === enabled) { + return; + } + store.set('brush', { + ...store.state.brush, + isZoomBrushEnabled: enabled, }); }); diff --git a/packages/x-charts/src/internals/plugins/featurePlugins/useChartCartesianAxis/useChartCartesianAxis.ts b/packages/x-charts/src/internals/plugins/featurePlugins/useChartCartesianAxis/useChartCartesianAxis.ts index 03f14a2a2c303..f9f7adb9d0145 100644 --- a/packages/x-charts/src/internals/plugins/featurePlugins/useChartCartesianAxis/useChartCartesianAxis.ts +++ b/packages/x-charts/src/internals/plugins/featurePlugins/useChartCartesianAxis/useChartCartesianAxis.ts @@ -1,6 +1,7 @@ 'use client'; import * as React from 'react'; import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; +import { useStoreEffect } from '@mui/x-internals/store'; import { useAssertModelConsistency } from '@mui/x-internals/useAssertModelConsistency'; import { warnOnce } from '@mui/x-internals/warning'; import { PointerGestureEventData } from '@mui/x-internal-gestures/core'; @@ -16,7 +17,6 @@ import { getAxisIndex } from './getAxisValue'; import { getSVGPoint } from '../../../getSVGPoint'; import { selectorChartsInteractionIsInitialized } from '../useChartInteraction'; import { selectorChartAxisInteraction } from './useChartCartesianInteraction.selectors'; -import { useLazySelectorEffect } from '../../utils/useLazySelectorEffect'; import { checkHasInteractionPlugin } from '../useChartInteraction/checkHasInteractionPlugin'; export const useChartCartesianAxis: ChartPlugin> = ({ @@ -61,16 +61,7 @@ export const useChartCartesianAxis: ChartPlugin { if (params.highlightedAxis !== undefined) { - store.update((prevState) => { - if (prevState.controlledCartesianAxisHighlight === params.highlightedAxis) { - return prevState; - } - - return { - ...prevState, - controlledCartesianAxisHighlight: params.highlightedAxis, - }; - }); + store.set('controlledCartesianAxisHighlight', params.highlightedAxis); } }, [store, params.highlightedAxis]); @@ -83,23 +74,22 @@ export const useChartCartesianAxis: ChartPlugin ({ - ...prev, - cartesianAxis: { - ...prev.cartesianAxis, - x: defaultizeXAxis(xAxis, dataset), - y: defaultizeYAxis(yAxis, dataset), - }, - })); + store.set('cartesianAxis', { + x: defaultizeXAxis(xAxis, dataset), + y: defaultizeYAxis(yAxis, dataset), + }); }, [seriesConfig, drawingArea, xAxis, yAxis, dataset, store]); const usedXAxis = xAxisIds[0]; const usedYAxis = yAxisIds[0]; - useLazySelectorEffect( + useStoreEffect( store, selectorChartAxisInteraction, (prevAxisInteraction, nextAxisInteraction) => { + if (!onHighlightedAxisChange) { + return; + } if (Object.is(prevAxisInteraction, nextAxisInteraction)) { return; } @@ -118,7 +108,6 @@ export const useChartCartesianAxis: ChartPlugin = const defaultYAxisId = yAxisIds[0]; useEnhancedEffect(() => { - store.update((prev) => - prev.voronoi.isVoronoiEnabled === !disableVoronoi - ? prev - : { - ...prev, - voronoi: { - isVoronoiEnabled: !disableVoronoi, - }, - }, - ); + store.set('voronoi', { isVoronoiEnabled: !disableVoronoi }); }, [store, disableVoronoi]); React.useEffect(() => { @@ -229,23 +220,11 @@ export const useChartClosestPoint: ChartPlugin = // Instance implementation const enableVoronoiCallback = useEventCallback(() => { - store.update((prev) => ({ - ...prev, - voronoi: { - ...prev.voronoi, - isVoronoiEnabled: true, - }, - })); + store.set('voronoi', { isVoronoiEnabled: true }); }); const disableVoronoiCallback = useEventCallback(() => { - store.update((prev) => ({ - ...prev, - voronoi: { - ...prev.voronoi, - isVoronoiEnabled: false, - }, - })); + store.set('voronoi', { isVoronoiEnabled: false }); }); return { diff --git a/packages/x-charts/src/internals/plugins/featurePlugins/useChartHighlight/useChartHighlight.ts b/packages/x-charts/src/internals/plugins/featurePlugins/useChartHighlight/useChartHighlight.ts index dea58cd5ce62d..67820d504824f 100644 --- a/packages/x-charts/src/internals/plugins/featurePlugins/useChartHighlight/useChartHighlight.ts +++ b/packages/x-charts/src/internals/plugins/featurePlugins/useChartHighlight/useChartHighlight.ts @@ -15,17 +15,9 @@ export const useChartHighlight: ChartPlugin = ({ sto }); useEnhancedEffect(() => { - store.update((prevState) => - prevState.highlight.item === params.highlightedItem - ? prevState - : { - ...prevState, - highlight: { - ...prevState.highlight, - item: params.highlightedItem, - }, - }, - ); + if (store.state.highlight.item !== params.highlightedItem) { + store.set('highlight', { ...store.state.highlight, item: params.highlightedItem }); + } }, [store, params.highlightedItem]); const clearHighlight = useEventCallback(() => { @@ -35,7 +27,7 @@ export const useChartHighlight: ChartPlugin = ({ sto return; } - store.update((prev) => ({ ...prev, highlight: { item: null, lastUpdate: 'pointer' } })); + store.set('highlight', { item: null, lastUpdate: 'pointer' }); }); const setHighlight = useEventCallback((newItem: HighlightItemData) => { @@ -46,7 +38,7 @@ export const useChartHighlight: ChartPlugin = ({ sto } params.onHighlightChange?.(newItem); - store.update((prev) => ({ ...prev, highlight: { item: newItem, lastUpdate: 'pointer' } })); + store.set('highlight', { item: newItem, lastUpdate: 'pointer' }); }); return { diff --git a/packages/x-charts/src/internals/plugins/featurePlugins/useChartInteraction/useChartInteraction.ts b/packages/x-charts/src/internals/plugins/featurePlugins/useChartInteraction/useChartInteraction.ts index fb6f6437c5b5a..002901041c4ba 100644 --- a/packages/x-charts/src/internals/plugins/featurePlugins/useChartInteraction/useChartInteraction.ts +++ b/packages/x-charts/src/internals/plugins/featurePlugins/useChartInteraction/useChartInteraction.ts @@ -14,86 +14,57 @@ import { export const useChartInteraction: ChartPlugin = ({ store }) => { const cleanInteraction = useEventCallback(function cleanInteraction() { - store.update((prev) => { - return { - ...prev, - interaction: { ...prev.interaction, pointer: null, item: null }, - }; - }); + store.set('interaction', { ...store.state.interaction, pointer: null, item: null }); }); const removeItemInteraction = useEventCallback(function removeItemInteraction( itemToRemove?: ChartItemIdentifier, ) { - store.update((prev) => { - const prevItem = prev.interaction.item; + const prevItem = store.state.interaction.item; - if (!itemToRemove) { - // Remove without taking care of the current item - return prevItem === null - ? prev - : { - ...prev, - interaction: { - ...prev.interaction, - item: null, - }, - }; + if (!itemToRemove) { + // Remove without taking care of the current item + if (prevItem !== null) { + store.set('interaction', { ...store.state.interaction, item: null }); } + return; + } - if ( - prevItem === null || - Object.keys(itemToRemove).some( - (key) => - itemToRemove[key as keyof typeof itemToRemove] !== - prevItem[key as keyof typeof prevItem], - ) - ) { - // The current item is already different from the one to remove. No need to clean it. - return prev; - } + if ( + prevItem === null || + Object.keys(itemToRemove).some( + (key) => + itemToRemove[key as keyof typeof itemToRemove] !== prevItem[key as keyof typeof prevItem], + ) + ) { + // The current item is already different from the one to remove. No need to clean it. + return; + } - return { - ...prev, - interaction: { - ...prev.interaction, - item: null, - }, - }; - }); + store.set('interaction', { ...store.state.interaction, item: null }); }); const setItemInteraction = useEventCallback(function setItemInteraction( newItem: ChartItemIdentifierWithData, context: { interaction: InteractionUpdateSource }, ) { - store.update((prev) => { - if (fastObjectShallowCompare(prev.interaction.item, newItem)) { - return prev; - } - - return { - ...prev, - interaction: { - ...prev.interaction, - lastUpdate: context.interaction, - item: newItem, - }, - }; - }); + if (!fastObjectShallowCompare(store.state.interaction.item, newItem)) { + store.set('interaction', { + ...store.state.interaction, + lastUpdate: context.interaction, + item: newItem, + }); + } }); const setPointerCoordinate = useEventCallback(function setPointerCoordinate( coordinate: Coordinate | null, ) { - store.update((prev) => ({ - ...prev, - interaction: { - ...prev.interaction, - pointer: coordinate, - lastUpdate: coordinate !== null ? 'pointer' : prev.interaction.lastUpdate, - }, - })); + store.set('interaction', { + ...store.state.interaction, + pointer: coordinate, + lastUpdate: coordinate !== null ? 'pointer' : store.state.interaction.lastUpdate, + }); }); return { diff --git a/packages/x-charts/src/internals/plugins/featurePlugins/useChartKeyboardNavigation/useChartKeyboardNavigation.ts b/packages/x-charts/src/internals/plugins/featurePlugins/useChartKeyboardNavigation/useChartKeyboardNavigation.ts index 6e681239dd0d4..5fb6ca5185693 100644 --- a/packages/x-charts/src/internals/plugins/featurePlugins/useChartKeyboardNavigation/useChartKeyboardNavigation.ts +++ b/packages/x-charts/src/internals/plugins/featurePlugins/useChartKeyboardNavigation/useChartKeyboardNavigation.ts @@ -107,18 +107,12 @@ export const useChartKeyboardNavigation: ChartPlugin { const removeFocus = useEventCallback(function removeFocus() { - store.update((state) => { - if (state.keyboardNavigation.item === null) { - return state; - } - return { - ...state, - keyboardNavigation: { - ...state.keyboardNavigation, - item: null, - }, - }; - }); + if (store.state.keyboardNavigation.item !== null) { + store.set('keyboardNavigation', { + ...store.state.keyboardNavigation, + item: null, + }); + } }); React.useEffect(() => { @@ -129,47 +123,42 @@ export const useChartKeyboardNavigation: ChartPlugin { - let newFocusedItem = prevState.keyboardNavigation.item; - - switch (event.key) { - case 'ArrowRight': - newFocusedItem = getNextIndexFocusedItem(prevState); - break; - case 'ArrowLeft': - newFocusedItem = getPreviousIndexFocusedItem(prevState); - break; - case 'ArrowDown': { - newFocusedItem = getPreviousSeriesFocusedItem(prevState); - break; - } - case 'ArrowUp': { - newFocusedItem = getNextSeriesFocusedItem(prevState); - break; - } - default: - break; + let newFocusedItem = store.state.keyboardNavigation.item; + switch (event.key) { + case 'ArrowRight': + newFocusedItem = getNextIndexFocusedItem(store.state); + break; + case 'ArrowLeft': + newFocusedItem = getPreviousIndexFocusedItem(store.state); + break; + case 'ArrowDown': { + newFocusedItem = getPreviousSeriesFocusedItem(store.state); + break; } - - if (newFocusedItem !== prevState.keyboardNavigation.item) { - event.preventDefault(); - return { - ...prevState, - ...(prevState.highlight && { - highlight: { ...prevState.highlight, lastUpdate: 'keyboard' }, - }), - ...(prevState.interaction && { - interaction: { ...prevState.interaction, lastUpdate: 'keyboard' }, - }), - keyboardNavigation: { - ...prevState.keyboardNavigation, - item: newFocusedItem, - }, - }; + case 'ArrowUp': { + newFocusedItem = getNextSeriesFocusedItem(store.state); + break; } + default: + break; + } - return prevState; - }); + if (newFocusedItem !== store.state.keyboardNavigation.item) { + event.preventDefault(); + + store.update({ + ...(store.state.highlight && { + highlight: { ...store.state.highlight, lastUpdate: 'keyboard' }, + }), + ...(store.state.interaction && { + interaction: { ...store.state.interaction, lastUpdate: 'keyboard' }, + }), + keyboardNavigation: { + ...store.state.keyboardNavigation, + item: newFocusedItem, + }, + }); + } } element.addEventListener('keydown', keyboardHandler); @@ -180,22 +169,16 @@ export const useChartKeyboardNavigation: ChartPlugin - store.update((prev) => - prev.keyboardNavigation.enableKeyboardNavigation === params.enableKeyboardNavigation - ? prev - : { - ...prev, - keyboardNavigation: { - ...prev.keyboardNavigation, - enableKeyboardNavigation: !!params.enableKeyboardNavigation, - }, - }, - ), - - [store, params.enableKeyboardNavigation], - ); + useEnhancedEffect(() => { + if ( + store.state.keyboardNavigation.enableKeyboardNavigation !== params.enableKeyboardNavigation + ) { + store.set('keyboardNavigation', { + ...store.state.keyboardNavigation, + enableKeyboardNavigation: !!params.enableKeyboardNavigation, + }); + } + }, [store, params.enableKeyboardNavigation]); return {}; }; diff --git a/packages/x-charts/src/internals/plugins/featurePlugins/useChartPolarAxis/useChartPolarAxis.ts b/packages/x-charts/src/internals/plugins/featurePlugins/useChartPolarAxis/useChartPolarAxis.ts index 3b1a86658d887..b9429822da510 100644 --- a/packages/x-charts/src/internals/plugins/featurePlugins/useChartPolarAxis/useChartPolarAxis.ts +++ b/packages/x-charts/src/internals/plugins/featurePlugins/useChartPolarAxis/useChartPolarAxis.ts @@ -70,14 +70,11 @@ export const useChartPolarAxis: ChartPlugin> = ( return; } - store.update((prev) => ({ - ...prev, - polarAxis: { - ...prev.polarAxis, - rotation: defaultizeAxis(rotationAxis, dataset, 'rotation'), - radius: defaultizeAxis(radiusAxis, dataset, 'radius'), - }, - })); + store.set('polarAxis', { + ...store.state.polarAxis, + rotation: defaultizeAxis(rotationAxis, dataset, 'rotation'), + radius: defaultizeAxis(radiusAxis, dataset, 'radius'), + }); }, [seriesConfig, drawingArea, rotationAxis, radiusAxis, dataset, store]); const svg2rotation = React.useMemo( diff --git a/packages/x-charts/src/internals/plugins/featurePlugins/useChartZAxis/useChartZAxis.ts b/packages/x-charts/src/internals/plugins/featurePlugins/useChartZAxis/useChartZAxis.ts index 6ae5dcc95b321..f04c27b32d8db 100644 --- a/packages/x-charts/src/internals/plugins/featurePlugins/useChartZAxis/useChartZAxis.ts +++ b/packages/x-charts/src/internals/plugins/featurePlugins/useChartZAxis/useChartZAxis.ts @@ -84,10 +84,7 @@ export const useChartZAxis: ChartPlugin = ({ params, sto return; } - store.update((prev) => ({ - ...prev, - zAxis: getZAxisState(zAxis, dataset), - })); + store.set('zAxis', getZAxisState(zAxis, dataset)); }, [zAxis, dataset, store]); return {}; diff --git a/packages/x-charts/src/internals/plugins/models/plugin.ts b/packages/x-charts/src/internals/plugins/models/plugin.ts index 0fa75f2785af7..55f4ea49cc03c 100644 --- a/packages/x-charts/src/internals/plugins/models/plugin.ts +++ b/packages/x-charts/src/internals/plugins/models/plugin.ts @@ -1,8 +1,9 @@ import * as React from 'react'; +import { Store } from '@mui/x-internals/store'; import type { MergeSignaturesProperty, OptionalIfEmpty } from './helpers'; import type { ChartCorePluginSignatures } from '../corePlugins'; -import { ChartStore } from '../utils/ChartStore'; import { ChartSeriesConfig } from './seriesConfig'; +import { ChartState } from './chart'; export interface ChartPluginOptions { /** @@ -125,9 +126,8 @@ export type ChartUsedInstance = $$signature: TSignature; }; -export type ChartUsedStore = ChartStore< - [TSignature, ...TSignature['dependencies']], - TSignature['optionalDependencies'] +export type ChartUsedStore = Store< + ChartState<[TSignature, ...TSignature['dependencies']], TSignature['optionalDependencies']> >; export type ChartPlugin = { diff --git a/packages/x-charts/src/internals/plugins/utils/ChartStore.ts b/packages/x-charts/src/internals/plugins/utils/ChartStore.ts deleted file mode 100644 index 0e303adc2c2b5..0000000000000 --- a/packages/x-charts/src/internals/plugins/utils/ChartStore.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { ChartState } from '../models/chart'; -import type { ChartAnyPluginSignature } from '../models/plugin'; - -type Listener = (value: T) => void; - -export type StoreUpdater< - TSignatures extends readonly ChartAnyPluginSignature[], - TOptionalSignatures extends readonly ChartAnyPluginSignature[] = [], -> = ( - prevState: ChartState, -) => ChartState; - -export class ChartStore< - TSignatures extends readonly ChartAnyPluginSignature[], - TOptionalSignatures extends readonly ChartAnyPluginSignature[] = [], -> { - public value: ChartState; - - private listeners: Set>>; - - constructor(value: ChartState) { - this.value = value; - this.listeners = new Set(); - } - - public subscribe = (fn: Listener>) => { - this.listeners.add(fn); - return () => { - this.listeners.delete(fn); - }; - }; - - public getSnapshot = () => { - return this.value; - }; - - public update = (updater: StoreUpdater) => { - const newState = updater(this.value); - if (newState !== this.value) { - this.value = newState; - this.listeners.forEach((l) => l(newState)); - } - }; -} diff --git a/packages/x-charts/src/internals/plugins/utils/useLazySelectorEffect.ts b/packages/x-charts/src/internals/plugins/utils/useLazySelectorEffect.ts index 095d4ed6c06d7..1dfb37f6c0187 100644 --- a/packages/x-charts/src/internals/plugins/utils/useLazySelectorEffect.ts +++ b/packages/x-charts/src/internals/plugins/utils/useLazySelectorEffect.ts @@ -1,8 +1,8 @@ 'use client'; /* eslint-disable react-compiler/react-compiler */ import * as React from 'react'; +import type { Store } from '@mui/x-internals/store'; import useLazyRef from '@mui/utils/useLazyRef'; -import type { ChartStore } from './ChartStore'; import { ChartAnyPluginSignature, ChartState } from '../models'; const noop = () => {}; @@ -11,7 +11,7 @@ export function useLazySelectorEffect< TSignatures extends readonly ChartAnyPluginSignature[], Value, >( - store: ChartStore, + store: Store>, selector: (state: ChartState) => Value, effect: (previous: Value, next: Value) => void, /** @@ -32,7 +32,7 @@ export function useLazySelectorEffect< // `useLazyRef` typings are incorrect, `params` should not be optional function initialize(params?: { - store: ChartStore; + store: Store>; selector: (state: ChartState) => Value; skip?: boolean; }) { @@ -65,7 +65,7 @@ function initialize(null); const innerSvgRef = React.useRef(null); - const storeRef = React.useRef | null>(null); + const storeRef = React.useRef>>(null); if (storeRef.current == null) { // eslint-disable-next-line react-compiler/react-compiler globalId += 1; @@ -80,7 +80,7 @@ export function useCharts< ); } }); - storeRef.current = new ChartStore(initialState); + storeRef.current = new Store>(initialState); } const runPlugin = (plugin: ChartPlugin) => { @@ -88,7 +88,9 @@ export function useCharts< instance, params: pluginParams, plugins: plugins as ChartPlugin[], - store: storeRef.current as ChartStore, + store: storeRef.current as Store< + ChartState & UseChartInteractionState + >, svgRef: innerSvgRef, chartRootRef: innerChartRootRef, seriesConfig, @@ -107,8 +109,7 @@ export function useCharts< const contextValue = React.useMemo( () => ({ - store: storeRef.current as ChartStore & - UseChartInteractionState, + store: storeRef.current!, publicAPI: publicAPI.current, instance, svgRef: innerSvgRef, diff --git a/packages/x-charts/src/internals/store/useSelector.ts b/packages/x-charts/src/internals/store/useSelector.ts index 518fe59ef3618..963986066ba38 100644 --- a/packages/x-charts/src/internals/store/useSelector.ts +++ b/packages/x-charts/src/internals/store/useSelector.ts @@ -1,9 +1,9 @@ /* We need to import the shim because React 17 does not support the `useSyncExternalStore` API. * More info: https://github.com/mui/mui-x/issues/18303#issuecomment-2958392341 */ import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'; +import { Store } from '@mui/x-internals/store'; import { ChartAnyPluginSignature, ChartState } from '../plugins/models'; import { ChartsSelector } from '../plugins/utils/selectors'; -import { ChartStore } from '../plugins/utils/ChartStore'; const defaultCompare = Object.is; @@ -13,7 +13,7 @@ export const useSelector = < TArgs extends readonly any[], TResult = unknown, >( - store: ChartStore, + store: Store>, selector: ChartsSelector, args = [] as [TArgs] extends [never] ? [] : TArgs, equals: (a: TResult, b: TResult) => boolean = defaultCompare, diff --git a/packages/x-charts/src/internals/store/useStore.ts b/packages/x-charts/src/internals/store/useStore.ts index 58aa3633a7738..355340ac5df33 100644 --- a/packages/x-charts/src/internals/store/useStore.ts +++ b/packages/x-charts/src/internals/store/useStore.ts @@ -1,14 +1,12 @@ +import { Store } from '@mui/x-internals/store'; import { useChartContext } from '../../context/ChartProvider'; -import { ChartStore } from '../plugins/utils/ChartStore'; -import { UseChartInteractionSignature } from '../plugins/featurePlugins/useChartInteraction'; -import { UseChartHighlightSignature } from '../plugins/featurePlugins/useChartHighlight'; -import { ChartAnyPluginSignature } from '../plugins/models'; +import { ChartAnyPluginSignature, ChartState } from '../plugins/models'; // This hook should be removed because user and us should not interact with the store directly, but with public/private APIs -export function useStore(): ChartStore< - [...TSignatures, UseChartInteractionSignature, UseChartHighlightSignature] +export function useStore(): Store< + ChartState > { - const context = useChartContext(); + const context = useChartContext(); if (!context) { throw new Error( diff --git a/test/regressions/charts/BrushBar.tsx b/test/regressions/charts/BrushBar.tsx index 823e646a34497..59ad18fdbe1c1 100644 --- a/test/regressions/charts/BrushBar.tsx +++ b/test/regressions/charts/BrushBar.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { BarChart } from '@mui/x-charts/BarChart'; -import { useStore } from '@mui/x-charts/internals'; +import { UseChartBrushSignature, useStore } from '@mui/x-charts/internals'; import { ChartsBrushOverlay } from '@mui/x-charts/ChartsBrushOverlay'; const dataset = [ @@ -15,17 +15,19 @@ const dataset = [ ]; function BrushSimulator() { - const store = useStore(); + const store = useStore<[UseChartBrushSignature]>(); React.useEffect(() => { // Simulate brush selection for visual regression test - store.update((prevState) => ({ - ...prevState, + store.update({ brush: { + enabled: true, + preventTooltip: true, + preventHighlight: true, start: { x: 100, y: 50 }, current: { x: 300, y: 300 }, }, - })); + }); }, [store]); return ; diff --git a/test/regressions/charts/BrushScatter.tsx b/test/regressions/charts/BrushScatter.tsx index 75abe7f440e3b..d090178d879c7 100644 --- a/test/regressions/charts/BrushScatter.tsx +++ b/test/regressions/charts/BrushScatter.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { ScatterChart } from '@mui/x-charts/ScatterChart'; import { ChartsBrushOverlay } from '@mui/x-charts/ChartsBrushOverlay'; -import { useStore } from '@mui/x-charts/internals'; +import { UseChartBrushSignature, useStore } from '@mui/x-charts/internals'; const scatterData = [ { x: 10, y: 20, id: 1 }, @@ -15,17 +15,19 @@ const scatterData = [ ]; function BrushSimulator() { - const store = useStore(); + const store = useStore<[UseChartBrushSignature]>(); React.useEffect(() => { // Simulate brush selection for visual regression test - store.update((prevState) => ({ - ...prevState, + store.update({ brush: { + enabled: true, + preventTooltip: true, + preventHighlight: true, start: { x: 150, y: 150 }, current: { x: 350, y: 250 }, }, - })); + }); }, [store]); return ;