-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[charts] Migrate some animations from react-spring #16961
[charts] Migrate some animations from react-spring #16961
Conversation
Thanks for adding a type label to the PR! 👍 |
Deploy preview: https://deploy-preview-16961--material-ui-x.netlify.app/ |
CodSpeed Performance ReportMerging #16961 will improve performances by 39.89%Comparing Summary
Benchmarks breakdown
|
I think I'll need to update the Screen.Recording.2025-03-14.at.12.14.53.movThe |
Insn't |
No, Here's a tool to use the |
The linear function is different to the linear curve, but they have the same name 🫠 |
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
fd554a1
to
08f4c12
Compare
* transition. | ||
* | ||
* This runs on every render, so it must be light. */ | ||
React.useLayoutEffect(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm surprised the server does nto complain that useLayoutEffect
does not exist on server
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. I have no idea why I'm not seeing a warning 😅
Could it be because the playground is running on NextJS pages router?
Nevertheless, I think we might have to use an "isomorphic" layout effect, as none of these options apply here. It would be unfortunate to introduce a component just to support SSR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree about the useIsomorphic. About the abscence of error, it seems to be related to React v19. I tried with v18 and I got the famous
Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://reactjs.org/link/uselayouteffect-ssr for common fixes.
at AnimatedLine (/home/alexandre/dev/mui/mui-x/packages/x-charts/src/LineChart/AnimatedLine.tsx:46:13)
Otherwise the SSR looks good 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done 👍
4d908bc
to
e8fcc72
Compare
return; | ||
} | ||
|
||
// If props aren't the same, interrupt the transition and fall through to start a new animation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand why we need to restart an animation if props get updated. If the color of a bar is updated. I don't see a reason to restart an animation about it's x
attribute
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These aren't all props. These are only props that will be animated. We need to restart the animation when those props change.
IMO we shouldn't expose useAnimate
directly. This is for internal use only. What we export is useAnimatePieArc
or useAnimateLine
and those will only pass the animatable props.
d5bfbe9
to
c3c03cb
Compare
React 18 tests are failing for a curious reason, so leaving this comment for the future. React 19 introduced double calling of ref callbacks when It is working on React 18, though, which is why the tests were failing: the |
const lastElement = elementRef.current; | ||
|
||
if (lastElement) { | ||
interrupt(lastElement, transitionName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This breaks in Strict Mode. D3 transitions aren't resumable, so if we interrupt them we need to start another one, which we aren't at the moment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Due to this, I think I'll need to implement resumable transitions, which D3 transitions doesn't support.
The idea is to store a transition in a ref and stop it on ref cleanups. When the ref callback is called again with an element, we check if it's the same DOM element and same props: if so, resume transition; if not, start new transition.
fd24455
to
5516fd5
Compare
* The props object also accepts a `ref` which will be merged with the ref returned from this hook. This means you can | ||
* pass the ref returned by this hook to the `path` element and the `ref` provided as argument will also be called. */ | ||
export function useAnimatePieArc( | ||
props: PieArcAnimatedProps & { ref?: React.RefObject<SVGPathElement> }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was unsure whether to pass ref as a normal prop or a second argument. Since ref is now a normal prop I'm inclined to keep this API so a user can pass the props directly. Otherwise, they'll need to destructure ref from the props to pass it as a second parameter.
This means that React 18 users will need to do useAnimatePieArc({ ...props, ref })
, which is unfortunate.
@JCQuintas @alexfauquette let me know what you think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be a small issue. Should we allow useAnimatePieArc({ ...props, ref? }, ref?)
And use whichever is passed in?
Right now charts don't work properly with react 19 anyways. Though with this PR it comes closer being react 19-able.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we allow
useAnimatePieArc({ ...props, ref? }, ref?)
And use whichever is passed in?
We can, but then we'd have to deprecate this behavior in future releases, that's why I'm more inclined towards spreading props. As people migrate to React >=19, the spreading becomes unnecessary.
Do you think it would be that cumbersome to spread the props and add the ref for React <19 that it's worth adding this behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how far we would want to push this 😅
It would be possible to properly type it for different react versions by using something like this:
type React19<R19, R18> =
Exclude<ReturnType<React.RefCallback<number>>, void> extends never ? R18 : R19;
type UseAnimatePieArcParams = React19<
[params: { something: number } & { ref?: React.Ref<SVGPathElement> }],
[params: { something: number }, ref?: React.Ref<SVGPathElement>]
>;
export function useAnimatePieArc(...params: UseAnimatePieArcParams): any;
export function useAnimatePieArc(
params: { something: number; ref?: React.Ref<SVGPathElement> },
ref?: React.Ref<SVGPathElement>,
): any {
const internalRef = React.useRef<SVGPathElement>(null);
const myRef = params.ref ?? ref ?? internalRef;
return myRef;
}
const WorksIn19 = () => {
const internalRef = React.useRef<SVGPathElement>(null);
const ref = useAnimatePieArc({ something: 1, ref: internalRef });
return ref;
};
const WorksIn18 = () => {
const internalRef = React.useRef<SVGPathElement>(null);
const ref = useAnimatePieArc({ something: 1 }, internalRef);
return ref;
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, but if we wanted to remove the second parameter it would still be a breaking change.
I think the number of people using our exported animation hooks will be relatively low, so I think it would be better to keep the ref-inside-props API. It is slightly annoying to spread the ref into the props object, but it's temporary and it will stop being problem as soon as users upgrade to React 19.
What do you think? Do you think it's that cumbersome that's worth us having two code paths to avoid an object spread for some users?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I think you are right. I don't think this will be heavily used enough to provide two different APIs, and the use-case where the user will need their own access to ref would be even lower
70be09f
to
a9042ae
Compare
…ion jumps to end.
6836d2c
to
94610d8
Compare
It was there already 😄 any improvement suggestion? |
bbe1d16
to
28eef59
Compare
4a6b3b6
to
5a8ef49
Compare
5a8ef49
to
d1b5f1c
Compare
|
Related to #16281.
This PR proposes a new way to animate components in the charts library.
Where possible, we should use CSS animations and transitions. In the cases where using CSS animations isn't possible, this PR introduces a new hook
useAnimate
which allows for JS-based animation.The hook uses a ref to reduce re-renders. This
useAnimate
hook is low-level and should not be exported. Higher-level and exportable hooks should be created for each specific animation (seeuseAnimateLine
,useAnimatePieArc
).Open Questions
How to handle versioning?
This is a breaking change because the props passed to slots changed from
SpringValue<T>
toT
. The window for merging this into v8 is closing, but we probably need robust testing to ensure this is a good alternative to React Spring.Changelog
Removed
react-spring
from the following slots:pieArc
: used directly inPieArcPlot
and transitively inPieChart
;mark
used directly inMarkPlot
and transitively inLineChart
;line
used directly inLineElement
, and transitively inLinePlot
,LineChart
andSparkLineChart
(ifplotType
is 'line').As a result, the
SpringValue
wrapper in some ofpieArc
's slot props were removed. This means that the propscornerRadius
,endAngle
,innerRadius
,outerRadius
,paddingAngle
andstartAngle
are nownumber
instead ofSpringValue<number>
.Additionally, the
pieArc
slot now receives askipAnimation
prop to configure whether animations should be enabled or disabled.