-
-
Notifications
You must be signed in to change notification settings - Fork 32.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
[ButtonBase] Fix when changing enableRipple prop from false to true #19667
[ButtonBase] Fix when changing enableRipple prop from false to true #19667
Conversation
Details of bundle changes.Comparing: 9532f26...1f124be
|
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.
Can reproduce as described. We still need a reduced example that is testable. It's not entirely clear why this wouldn't be set.
The issue is that we might miss pulsating.
Yeah, but it is anyway the bad practice to not check ref.current. Especially if you are calling it in useEffect, because it can be called when the element is actually unmounted |
Not really no. It might make more sense to throw a more descriptive error, sure. But if you expect the ref to be set, then a defensive check might hide actual issues. That's why I'm asking for a test: To be sure that this is a legitimate state our component can be in or if there's a user mistake and we should throw a descriptive error. |
No, it is a bad practice – any code that can adjust null in this particular moment should be catched for nulls. If you will use typescript for |
Could you engage with the point that we might want to crash because of invalid input? |
It took me most of the day, but I found the reason https://codesandbox.io/s/wandering-night-ff46i The problem is effect dependency I don't know why it was reproducing for me only after I know that it is a |
@eps1lon how I can write a test for this case? I expect this test to be written as e2e test, that running in real browser. But cannot figure out how can I write steps to control karma tests, could you help here? |
I'm a bit lost about what's happening here. The if-block is only entered when I seem to remember some issue with overlapping ref and effect phases when looking at parents and their children. This might be the issue here. In that case we can only add a warning as far as I can tell.
Let's see if this only happens in a browser first. I don't think we need one. |
It seems to be an issue with a delay NoSsr introduces. Remove the component and it works OK. |
What about the following: we replace the NoSsr usage with a custom hook? Adding a defensive check would "hide" this underlying issue. |
I am not sure what you want me to do. I can add a warning and test using testing-lib, but are you sure it is a right idea trying to test dom |
JSDOM is close enough for focus managment. What are you missing from JSDOM regarding |
Here is an interesting test case:
The challenge would be to make it pass while keeping the SSR boost :)? it.only('--', () => {
function App() {
const [enableRipple, setRipple] = React.useState(false);
return (
<div>
<button
data-testid="trigger"
onClick={() => {
setRipple(true);
}}
>
Trigger crash
</button>
<ButtonBase
autoFocus
TouchRippleProps={{
classes: {
ripplePulsate: 'ripple-pulsate',
},
}}
focusRipple
disableRipple={!enableRipple}
>
1
</ButtonBase>
<ButtonBase autoFocus focusRipple>
2
</ButtonBase>
</div>
);
}
const { container, getByTestId } = render(<App />);
fireEvent.click(getByTestId('trigger'));
expect(container.querySelectorAll('.ripple-pulsate')).to.have.lengthOf(1);
}); |
Yeah that was it. ~NoSsr has a layout effect. When a layout effect runs every queued passive effect runs as well. The My guess is that
|
The test from @oliviertassinari passes with --- a/packages/material-ui/src/ButtonBase/ButtonBase.js
+++ b/packages/material-ui/src/ButtonBase/ButtonBase.js
@@ -302,12 +302,12 @@ const ButtonBase = React.forwardRef(function ButtonBase(props, ref) {
{...other}
>
{children}
- {!disableRipple && !disabled ? (
- <NoSsr>
- {/* TouchRipple is only needed client-side, x2 boost on the server. */}
+ <NoSsr>
+ {/* TouchRipple is only needed client-side, x2 boost on the server. */}
+ {!disableRipple && !disabled && (
<TouchRipple ref={rippleRef} center={centerRipple} {...TouchRippleProps} />
- </NoSsr>
- ) : null}
+ )}
+ </NoSsr>
</ComponentProp>
);
}); |
@eps1lon Nice! I wonder if this wouldn't be an opportunity to replace this NoSsr with a hook. |
Ouch, there is a conflict, somewhere between the tests. |
@oliviertassinari trying to debug this, can you point me to the right direction? |
@dmtrKovalenko I could isolate the conflict down to between
Which alternative do you have in mind? I believe the motivation is so we can use |
const [enableRipple, setRipple] = React.useState(false); | ||
|
||
React.useEffect(() => { | ||
if (buttonRef.current) { |
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 is the type of defensive checks I was working about that TypeScript encourage and that might hide root issues. @eps1lon what do 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 one is not hiding issue of test, you can check it and without changes this test will still crash
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.
Agree, I was wondering about the pattern in general, what should be our "baseline" (default approach) for this concern :)
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.
Do we even need this check here? What happens if App
mounts but the ref isn't instantiated?
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 check is for typescript compiler, to get rid of @ts-ignore
. Actually it is impossible inside the useEffect
(maybe only if somebody will specifically delete dom element after react commit phase and before effects are run?)
Right here it is not needed, but I am sure that in the code we have to guard the refs, even if it is hide bugs (IMHO such guards can't hide bugs, because e.g. in this particular issue element will not gain ripple focus if check was done - it is a bug as well, but no so critical as crash)
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.
it is a bug as well, but no so critical as crash)
So we agree that it shouldn't be guarded against. There was a defect in the code which we wouldn't have been able to detect with a defensive check.
As long as typescript is only a type checker + transpiler I don't care about @ts-ignore
in JSX. TypeScript is particularly bad with React. If it complains, we disable it. Just like with false positives in lint rules.
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.
We still need to get rid of this check in the test or handle the else case.
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.
👌 ok I will add throwing error in the else branch.
Cannot agree — we would be able. If there will be a guard, when enableRipple changes we simply not starting rippleEffect. It is an issue as well that can be noticed and fixed in the same way as current one.
But if some user will notice crash in the most basic Button
component — it will spoil the trust to material-ui as qualified and reliable tool.
I have strong opinion that if something can crash — you must not let it crash. Even if you will use more reliable type system like ReasonML — it will complain about refs, because we cannot trust DOM, like we cannot trust users and we cannot trust developers who uses our components.
I can propose you the following api for using in the core components.
assertReactDomRef(ref, ref => ref.focus())
// or using new asserts syntax
assertReactDomRef(ref)
ref.focus()
Which will assert ref is not null and if it’s missing warn user and ask to raise an issue. Thus our component will even look more clever :)
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.
As far as I understand the issue, this is an internal consideration. Regarding the impact userland, I think that a fast feedback loop should be preferred.
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.
@dmtrKovalenko I love the new throw error in the branch, it's a great edge in case the action prop stop working as expected, we get a clear error message in the test. Smart! In an older test, we were using // @ts-ignore
, which sounds great too.
But if some user will notice crash in the most basic Button component — it will spoil the trust to material-ui as qualified and reliable tool.
From my perspective, the ref effect should always run before a layout effect which also runs before an effect. The ref should always be defined. If it's not, then there is a deeper issue, that a defensive logic will hide, make it harder to uncover.
So in userland, I would recommend the usage of // @ts-ignore
or to throw, like in the test cases.
Reminder: Don't use github UI for longstanding PRs 😆 About mocha: What other framework would you use? Jest is no option because it can't run in a browser. |
I meant jest as test runner, but I got that you are using karma. Only one thing I know is better than mocha (as for me) is ava. But I don't think migration worth it :) |
Fixing crash if ref.current is null. We always need to check that
ref.current
is available before useReproduce the crash: