Material-ui's Switch component onChange handler is not firing

Viewed 17182

I've put some Switches in an app and they work fine. Then I put the Switches in another app, but they don't work when clicked.

Both apps are using the same component. Here it is working in one app:

enter image description here

And here's the other app, not working:

enter image description here

In the second app, the onChange handler doesn't seem to ever fire.

The code in both apps looks like the following:

<Switch
  checked={(console.log('checked:', status === 'visible'), status === 'visible')}
  onChange={(e, c) => console.log('event:', e, c)}
/>

In the first app I see the output of those console.logs, while in the second app I only see the initial console.log of the checked prop, but I never see any of the onChange prop.

I checked if any ancestor elements have click handlers, and I didn't find any that are returning false, calling stopPropagation, or calling preventDefault.

Notice in the gif that when I click, the ripple effect still works, so click handling is obviously still working.

Any ideas why onChange may not be firing?

UPDATE! I replaced the switches with regular <input type="checkbox"> elements, and it works great! See:

enter image description here

Looks to me like something is wrong with material-ui's <Switch> component. I have a hunch that I will investigate when I get a chance: there might be more than one React singleton in the application. I'll be back to post an update.

7 Answers

I think, this is a weird fix and it is working smoothly for me. So, instead of handleChange I am using handleClick. I am not using event here, instead I am passing a string which is obviously the name of the state or id in case of arrays.

<Switch
    checked={this.state.active}
    onClick={() => this.handleToggle('active')}
    value="active"
    inputProps={{ 'aria-label': 'secondary checkbox' }}
/>

handleToggle = (name: string) => {
    this.setState({ active: !this.state.active });
};

I tried handleChange, but the problem still persists. I hope this will get fixed soon.

I've had the same issue with Checkbox and Switch in the WordPress admin area.

Turns out, there was global CSS rule like:

input[type="checkbox"] {
  height: 1rem;
  width: 1rem;
}

Clicking the upper left corner of the element works, though.

As a solution, I reset some styles in my app root.

EDIT: Nevermind, I just put my whole app into shadow DOM. There are a few gotchas, I'll list them here:

  1. You have to provide a custom insertion point for Material-UI style elements inside the shadow DOM. In general, you have to make nothing gets put outside of you shadow DOM.
  2. You have to load/link the font inside the shadow DOM and outside the shadow DOM (the light DOM).
  3. Use ScopedCssBaseline instead of the global reset.
  4. Dialogs have to have their container prop specified.

This is how I've set things up with Material-UI:

// configure-shadow-dom.js

import { create } from 'jss';
import { jssPreset } from '@material-ui/core/styles';

const shadowHostId = 'my-app-root-id'

export const appRoot = document.createElement('div')
appRoot.setAttribute('id', 'app-root')

const styleInsertionPoint = document.createComment('jss-insertion-point')
export const jss = create({
    ...jssPreset(),
    insertionPoint: styleInsertionPoint,
})

const robotoFontLink = document.createElement('link')
robotoFontLink.setAttribute('rel', 'stylesheet')
robotoFontLink.setAttribute('href', 'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap')

const shadowHost = document.getElementById(shadowHostId)
shadowHost.attachShadow({ mode: 'open' })

const shadowRoot = shadowHost.shadowRoot
shadowRoot.appendChild(robotoFontLink)
shadowRoot.appendChild(styleInsertionPoint)
shadowRoot.appendChild(appRoot)


// index.jsx

import React from 'react';
import ReactDOM from 'react-dom';
import ScopedCssBaseline from '@material-ui/core/ScopedCssBaseline';
import { StylesProvider } from '@material-ui/core/styles';
import App from './App';
import { jss, appRoot } from './configure-shadow-dom';

ReactDOM.render(
    <React.StrictMode>
        <StylesProvider jss={jss}>
            <ScopedCssBaseline>
                <App />
            </ScopedCssBaseline>
        </StylesProvider>
    </React.StrictMode>,
    appRoot
);

It turns out that in my case, there was a CSS in the page, something like

.some-component { pointer-events: none; }
.some-component * { pointer-events: auto; }

where .some-component was containing my material-ui buttons and switches. I had to manually set pointer-events to all (or some value, I don't remember at at the moment) for the elements inside the switches.

So, that's one thing to look out for: check what pointer-events style is doing.

in Switch component, changing onChange to onClick worked for me:

<Switch
    checked={this.state.active}
    onClick={() => this.handleToggle('active')}
    value="active"
    inputProps={{ 'aria-label': 'secondary checkbox' }}
/>

Try this one checked or unchecked passed as a second argument.

<Switch
    checked={this.state.active}
    onClick={(e,flag) => this.handleToggle('active', flag)}
    value="active"
    inputProps={{ 'aria-label': 'secondary checkbox' }}
/>```

still running into similar issues getting an event from the switch. here's a quick fix using internal state (you should be using hooks now and functional components, if not you are missing out, but you can use setState to do what i'm showing here in a class component.)

const [switchChecked, setSwitchChecked] = useState(false);

.......

<Switch checked={switchChecked} onChange={() => {setSwitchChecked(!switchChecked);}}/>

you'll need to have a value for switchChecked in the components local state.

The issue is that you have to sniff the checked attribute from the event: event.target.checked

Full solution:

import { Switch } from '@material-ui/core';
import { useState } from 'react';

function App() {
  const [checked, setChecked] = useState(false);

  const switchHandler = (event) => {
    //THIS IS THE SOLUTION - use event.target.checked to get value of switch
    setChecked(event.target.checked);
  };

  return (
    <div>
      <Switch checked={checked} onChange={switchHandler} />
    </div>
  );
}

export default App;
Related