Repository: react-materialize/react-materialize
Pull request #958 (2019-11-26 20:31:26)

Button: use hooks
# Description

Part of #928 
Closes #918 as well.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

# How Has This Been Tested?

Updated both `spec`s and `snapshot`s

# Checklist:

- [x] I have commented my code, particularly in hard-to-understand areas
- [x] My changes generate no new warnings
- [x] I have not generated a new package version. (the maintainers will handle that)



Patch:
diff --git a/src/Button.js b/src/Button.js
index b2a4932..85918da 100644
--- a/src/Button.js
+++ b/src/Button.js
@@ -1,124 +1,109 @@
-import React, { Component } from 'react';
+import React, { Children, useRef, useEffect } from 'react';
 import Icon from './Icon';
 import PropTypes from 'prop-types';
 import constants from './constants';
 import cx from 'classnames';
 import idgen from './idgen';
 
-class Button extends Component {
-  constructor(props) {
-    super(props);
-    this.renderFab = this.renderFab.bind(this);
-  }
-
-  componentDidMount() {
-    if (typeof M === 'undefined') return;
-
-    const { tooltip, tooltipOptions = {}, fab } = this.props;
+const Button = ({
+  children,
+  className,
+  node,
+  fab,
+  modal,
+  flat,
+  floating,
+  large,
+  small,
+  disabled,
+  waves,
+  tooltip,
+  tooltipOptions = {},
+  icon,
+  ...props
+}) => {
+  const _btn = useRef(null);
+  const _fab = useRef(null);
+
+  useEffect(() => {
     if (tooltip) {
-      this.instance = M.Tooltip.init(this._btnEl, tooltipOptions);
+      const instance = M.Tooltip.init(_btn.current, tooltipOptions);
+
+      return () => {
+        instance && instance.destroy();
+      };
     }
+  }, [tooltip, tooltipOptions]);
 
+  useEffect(() => {
     if (fab) {
-      this.instance = M.FloatingActionButton.init(this._floatingActionBtn, fab);
-    }
-  }
+      const instance = M.FloatingActionButton.init(_fab.current, fab);
 
-  componentWillUnmount() {
-    if (this.instance) {
-      this.instance.destroy();
+      return () => {
+        instance && instance.destroy();
+      };
     }
-  }
+  }, [fab]);
 
-  render() {
-    const {
-      children,
-      className,
-      node,
-      fab,
-      modal,
-      flat,
-      floating,
-      large,
-      small,
-      disabled,
-      waves,
-      tooltip,
-      icon,
-      ...other
-    } = this.props;
+  const renderFab = classes => {
+    return (
+      <div
+        {...props}
+        ref={_fab.current}
+        className={cx('fixed-action-btn', {
+          toolbar: fab.toolbarEnabled
+        })}
+      >
+        <a className={classes}>{icon || <Icon>edit</Icon>}</a>
+        <ul>
+          {Children.map(children, child => (
+            <li key={idgen()}>{child}</li>
+          ))}
+        </ul>
+      </div>
+    );
+  };
 
-    delete other.tooltipOptions;
+  let C = node;
+  let classes = {
+    btn: true,
+    disabled,
+    'waves-effect': waves
+  };
 
-    let C = node;
-    let classes = {
-      btn: true,
-      disabled,
-      'waves-effect': waves
-    };
+  if (constants.WAVES.indexOf(waves) > -1) {
+    classes['waves-' + waves] = true;
+  }
 
-    if (constants.WAVES.indexOf(waves) > -1) {
-      classes['waves-' + waves] = true;
+  let styles = { flat, floating, large, small };
+  constants.STYLES.forEach(style => {
+    if (styles[style]) {
+      classes.btn = false;
+      classes['btn-' + style] = true;
     }
+  });
 
-    let styles = { flat, floating, large, small };
-    constants.STYLES.forEach(style => {
-      if (styles[style]) {
-        classes.btn = false;
-        classes['btn-' + style] = true;
-      }
-    });
-
-    if (modal) {
-      classes['modal-' + modal] = true;
-    }
-    if (fab) {
-      return this.renderFab(cx(classes, className));
-    } else {
-      return (
-        <C
-          {...other}
-          disabled={!!disabled}
-          onClick={this.props.onClick}
-          className={cx(classes, className)}
-          ref={el => (this._btnEl = el)}
-          data-tooltip={tooltip}
-        >
-          {icon}
-          {children}
-        </C>
-      );
-    }
+  if (modal) {
+    classes['modal-' + modal] = true;
   }
 
-  renderFab(classes) {
-    const {
-      fab,
-      floating,
-      large,
-      className,
-      icon = <Icon>edit</Icon>,
-      ...other
-    } = this.props;
-
+  if (fab) {
+    return renderFab(cx(classes, className));
+  } else {
     return (
-      <div
-        {...other}
-        ref={el => (this._floatingActionBtn = el)}
-        className={cx('fixed-action-btn', {
-          toolbar: fab && fab.toolbarEnabled
-        })}
+      <C
+        {...props}
+        disabled={disabled}
+        className={cx(classes, className)}
+        ref={_btn}
+        data-tooltip={tooltip}
       >
-        <a className={classes}>{icon}</a>
-        <ul>
-          {React.Children.map(this.props.children, child => {
-            return <li key={idgen()}>{child}</li>;
-          })}
-        </ul>
-      </div>
+        {icon}
+        {children}
+      </C>
     );
   }
-}
+};
 
 Button.propTypes = {
   children: PropTypes.node,


Test patch:
diff --git a/test/Button.spec.js b/test/Button.spec.js
index 9f63091..cb4bc42 100644
--- a/test/Button.spec.js
+++ b/test/Button.spec.js
@@ -100,12 +100,12 @@ describe('Button', () => {
     });
 
     test('initializes Button with tooltip', () => {
-      wrapper = shallow(<Button tooltip={tooltip}>Stuff</Button>);
+      wrapper = mount(<Button tooltip={tooltip}>Stuff</Button>);
       expect(tooltipInitMock).toHaveBeenCalled();
     });
 
     test('initializes Button with tooltip options', () => {
-      wrapper = shallow(
+      wrapper = mount(
         <Button tooltip={tooltip} tooltipOptions={tooltipOptions}>
           Stuff
         </Button>
@@ -114,29 +114,12 @@ describe('Button', () => {
     });
 
     test('destroyed when unmounted', () => {
-      wrapper = shallow(<Button tooltip={tooltip}>Stuff</Button>);
+      wrapper = mount(<Button tooltip={tooltip}>Stuff</Button>);
       wrapper.unmount();
       expect(tooltipInstanceDestroyMock).toHaveBeenCalled();
     });
   });
 
-  describe('undefined M', () => {
-    let __M;
-    beforeEach(() => {
-      __M = global.M;
-      global.M = undefined;
-    });
-    afterEach(() => {
-      global.M = __M;
-    });
-    test('doesnt throw without M', () => {
-      const tooltip = {};
-      expect(() =>
-        mount(<Button tooltip={tooltip}>Stuff</Button>)
-      ).not.toThrow();
-    });
-  });
-
   describe('with fixed action button', () => {
     const fabInitMock = jest.fn();
     const fabInstanceDestroyMock = jest.fn();
diff --git a/test/__snapshots__/Button.spec.js.snap b/test/__snapshots__/Button.spec.js.snap
index 27a58bd..7583163 100644
--- a/test/__snapshots__/Button.spec.js.snap
+++ b/test/__snapshots__/Button.spec.js.snap
@@ -3,7 +3,6 @@
 exports[`Button renders 1`] = `
 <button
   className="btn waves-effect waves-light"
-  disabled={false}
 >
   Stuff
 </button>
diff --git a/test/__snapshots__/Modal.spec.js.snap b/test/__snapshots__/Modal.spec.js.snap
index 7a53e6d..df1ba2f 100644
--- a/test/__snapshots__/Modal.spec.js.snap
+++ b/test/__snapshots__/Modal.spec.js.snap
@@ -96,7 +96,6 @@ exports[`<Modal /> renders a modal has children 1`] = `
         >
           <button
             className="waves-effect waves-green btn-flat modal-close"
-            disabled={false}
           >
             Close
           </button>
@@ -203,7 +202,6 @@ exports[`<Modal /> renders a trigger renders 1`] = `
         >
           <button
             className="waves-effect waves-green btn-flat modal-close"
-            disabled={false}
           >
             Close
           </button>
@@ -295,7 +293,6 @@ exports[`<Modal /> without a trigger renders 1`] = `
         >
           <button
             className="waves-effect waves-green btn-flat modal-close"
-            disabled={false}
           >
             Close
           </button>