-
-
Notifications
You must be signed in to change notification settings - Fork 32.3k
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
[material-ui] Performance: lazy Ripple #41061
Changes from all commits
71d1d9f
2d7b64e
cafe086
0e14e23
a71d975
a43448e
58b7afb
0febf69
1b6a64b
6ab75a4
5c4d423
ddf17e1
25711ee
86ac386
cfbe74f
bdf6c57
3c3441a
efeb466
4d18851
8192ac6
3388701
4dec342
af5900b
4335f71
e1ef060
a6b72d3
24b426f
4a730b4
88785a6
fca9fb6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ import { styled } from '../zero-styled'; | |
import { useDefaultProps } from '../DefaultPropsProvider'; | ||
import useForkRef from '../utils/useForkRef'; | ||
import useEventCallback from '../utils/useEventCallback'; | ||
import useLazyRipple from '../useLazyRipple'; | ||
import TouchRipple from './TouchRipple'; | ||
import buttonBaseClasses, { getButtonBaseUtilityClass } from './buttonBaseClasses'; | ||
|
||
|
@@ -109,8 +110,8 @@ const ButtonBase = React.forwardRef(function ButtonBase(inProps, ref) { | |
|
||
const buttonRef = React.useRef(null); | ||
|
||
const rippleRef = React.useRef(null); | ||
const handleRippleRef = useForkRef(rippleRef, touchRippleRef); | ||
const ripple = useLazyRipple(); | ||
const handleRippleRef = useForkRef(ripple.ref, touchRippleRef); | ||
|
||
const [focusVisible, setFocusVisible] = React.useState(false); | ||
if (disabled && focusVisible) { | ||
|
@@ -128,19 +129,13 @@ const ButtonBase = React.forwardRef(function ButtonBase(inProps, ref) { | |
[], | ||
); | ||
|
||
const [mountedState, setMountedState] = React.useState(false); | ||
const enableTouchRipple = ripple.shouldMount && !disableRipple && !disabled; | ||
|
||
React.useEffect(() => { | ||
setMountedState(true); | ||
}, []); | ||
|
||
const enableTouchRipple = mountedState && !disableRipple && !disabled; | ||
|
||
React.useEffect(() => { | ||
if (focusVisible && focusRipple && !disableRipple && mountedState) { | ||
rippleRef.current.pulsate(); | ||
if (focusVisible && focusRipple && !disableRipple) { | ||
ripple.pulsate(); | ||
} | ||
}, [disableRipple, focusRipple, focusVisible, mountedState]); | ||
}, [disableRipple, focusRipple, focusVisible, ripple]); | ||
|
||
function useRippleHandler(rippleAction, eventCallback, skipRippleAction = disableTouchRipple) { | ||
return useEventCallback((event) => { | ||
|
@@ -149,8 +144,8 @@ const ButtonBase = React.forwardRef(function ButtonBase(inProps, ref) { | |
} | ||
|
||
const ignore = skipRippleAction; | ||
if (!ignore && rippleRef.current) { | ||
rippleRef.current[rippleAction](event); | ||
if (!ignore) { | ||
ripple[rippleAction](event); | ||
} | ||
|
||
return true; | ||
|
@@ -212,9 +207,9 @@ const ButtonBase = React.forwardRef(function ButtonBase(inProps, ref) { | |
|
||
const handleKeyDown = useEventCallback((event) => { | ||
// Check if key is already down to avoid repeats being counted as multiple activations | ||
if (focusRipple && !event.repeat && focusVisible && rippleRef.current && event.key === ' ') { | ||
rippleRef.current.stop(event, () => { | ||
rippleRef.current.start(event); | ||
if (focusRipple && !event.repeat && focusVisible && event.key === ' ') { | ||
ripple.stop(event, () => { | ||
ripple.start(event); | ||
}); | ||
} | ||
|
||
|
@@ -243,15 +238,9 @@ const ButtonBase = React.forwardRef(function ButtonBase(inProps, ref) { | |
const handleKeyUp = useEventCallback((event) => { | ||
// calling preventDefault in keyUp on a <button> will not dispatch a click event if Space is pressed | ||
// https://codesandbox.io/p/sandbox/button-keyup-preventdefault-dn7f0 | ||
if ( | ||
focusRipple && | ||
event.key === ' ' && | ||
rippleRef.current && | ||
focusVisible && | ||
!event.defaultPrevented | ||
) { | ||
rippleRef.current.stop(event, () => { | ||
rippleRef.current.pulsate(event); | ||
if (focusRipple && event.key === ' ' && focusVisible && !event.defaultPrevented) { | ||
ripple.stop(event, () => { | ||
ripple.pulsate(event); | ||
}); | ||
} | ||
if (onKeyUp) { | ||
|
@@ -291,20 +280,6 @@ const ButtonBase = React.forwardRef(function ButtonBase(inProps, ref) { | |
|
||
const handleRef = useForkRef(ref, buttonRef); | ||
|
||
if (process.env.NODE_ENV !== 'production') { | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
React.useEffect(() => { | ||
if (enableTouchRipple && !rippleRef.current) { | ||
console.error( | ||
[ | ||
'MUI: The `component` prop provided to ButtonBase is invalid.', | ||
'Please make sure the children prop is rendered in this custom component.', | ||
].join('\n'), | ||
); | ||
} | ||
}, [enableTouchRipple]); | ||
} | ||
Comment on lines
-294
to
-306
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As the ripple is now lazy, using it for this devmode check doesn't make sense anymore. If someone has an idea for an alternative I can restore this warning. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The history: #20401. I guess taking the initial issue reproduction, and adding the warning where the code crash would do it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The initial issue would not happen anymore. With the previous implementation, not rendering the ripple in a custom button component would cause a crash when calling API methods because the ripple ref would be null. With the current implementation, the ripple API calls only go through once the ripple is mounted and its ref is not null; not rendering/mounting the ripple would simply create a promise that never resolves. So the check can't be used anymore, and I don't see an easy way to add back this warning. |
||
|
||
const ownerState = { | ||
...props, | ||
centerRipple, | ||
|
@@ -345,7 +320,6 @@ const ButtonBase = React.forwardRef(function ButtonBase(inProps, ref) { | |
> | ||
{children} | ||
{enableTouchRipple ? ( | ||
/* TouchRipple is only needed client-side, x2 boost on the server. */ | ||
romgrk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<TouchRipple ref={handleRippleRef} center={centerRipple} {...TouchRippleProps} /> | ||
) : null} | ||
</ButtonBaseRoot> | ||
|
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 will be super useful for all tests in the future 🎉
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.
Yes I'll post about it in slack once this is merged.
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 looks wrong in two separate ways:
render()
are supposed to be scoped to the element. It's not the case withuser
.It feels like we should deprecate this:
and we export
user
as a module like we do forscreen
.cc @Janpot for awareness. I landed here from: mui/mui-x#14142 (comment) from KevinVandy/material-react-table#1231 from https://x.com/XCSme/status/1829522518517956996