美化一下我们的Antd菜单吧,让每次点击都有波纹效果

3,150 阅读2分钟

这是我参与更文挑战的第7天,活动详情查看: 更文挑战

点击没效果,总感觉少点啥

用antd自带菜单导航,点击菜单项的时候并没有什么特别cool的效果,那我们就自己动手加上吧。

借鉴了网上一大佬的实现,并在其基础上补全完善

大佬的实现

在其基础上我补全了功能,使其能够解决我所遇到的问题:

首先菜单项是多项并且单项点击切换的,大佬的实现并不能很好完成这一点,当存在多个具备该特效的节点时,一旦刷新,所有的节点都会触发刷新,那就是满屏的波纹,这一点必须解决。


开始重构以达到期望

首先做成一个高阶组件,这样就可以使用ta来对其他组件进行包装,使其具有点击特效,方便使用。

组件命名为withMaterialHoc,先贴出组件的实现:

withMaterialHoc/index.js

import React from 'react';
import { Ripple, calcEventRelativePos } from './ripple';

let withMaterialHoc = (WrappedComponent) => {
  return class WithMaterial extends React.Component {
    constructor(props) {
      super(props)
      this.isSwitchIntl = false;
      this.state = {
        spawnData: "",
        clickCount: 0,
        isSwitchIntl: false
      }
    }

    handleClick = (event) => {
      this.setState({
        spawnData: calcEventRelativePos(event),
        time: Date.now(),
        clickCount: this.state.clickCount + 1
      });
    }

    shouldComponentUpdate(nextProps, nextState) {
      if (nextProps.intlData != this.props.intlData) {
        return true;
      }
      if (nextState.clickCount && (nextState.clickCount - this.state.clickCount === 1)) {
        return true
      } else {
        return false
      }
    }

    handleRippleEnd = () => {
      let value = this.state.clickCount - 1
      if (value < 0) {
        value = 0
      }
      this.setState({
        clickCount: value
      })
    }

    render() {
      const { spawnData } = this.state;
      const { className, style } = this.props;
      return (
        <div
          className={`g-btn ${className || ' '}`}
          onClick={this.handleClick}
          style={style}
        >
          <WrappedComponent {...this.props} />
          <Ripple handleRippleEnd={this.handleRippleEnd} spawnData={spawnData} />
        </div>
      );
    }
  };
}

export default withMaterialHoc;

ripple.js

import React, { useState, useEffect, useRef, Fragment,memo } from 'react';
import './index.css';
import { useSpring, animated } from 'react-spring';

function calcEventRelativePos(event) {
  const rect = event.target.getBoundingClientRect();
  return {
    x: event.clientX - rect.left,
    y: event.clientY - rect.top,
  };
}
function Ripple(props) {
  const [data, setData] = useState({ top: 0, left: 0, width: 0, height: 0 });
  const isInit = useRef(true);
  const rippleEl = useRef(null);
  const { spawnData, handleRippleEnd} = props;
  const rippleAnim = useSpring({
    from: {
      ...props.style,
      ...data,
      transform: 'scale(0)',
      opacity: 1,
    },
    to: !isInit.current ? { opacity: 0, transform: 'scale(2)' } : {},
    config: {
      duration: props.duration || 300,
    },
    onRest: () => {
      handleRippleEnd()
    },
    reset: true
  });

  useEffect(() => {
    if (isInit.current) {
      isInit.current = false;
    } else {
      const parentEl = rippleEl.current.parentElement;
      const size = Math.max(parentEl.offsetWidth, parentEl.offsetHeight);
      setData({
        width: size,
        height: size,
        top: spawnData.y - size / 2 || 0,
        left: spawnData.x - size / 2+50 || 50
      });
    }
  }, [spawnData]);
  return (
    <animated.span
          className="g-ripple"
          style={rippleAnim}
          ref={rippleEl}
        ></animated.span>
  );
}

Ripple = memo(Ripple)

export { Ripple, calcEventRelativePos };

分析:

大佬的实现为什么不行?

因为ripple是一个函数组件,每次父组件刷新,子组件就会刷新,从而出发了动画(实现用的是react-spring库,有兴趣可以深入了解)。

那么解决这个问题的关键就是,我点击哪个节点,哪个节点触发刷新,其他的节点不刷新

使用memo来控制函数组件的刷新

React.memo

React.memo 为高阶组件。它与 React.PureComponent 非常相似,但它适用于函数组件,但不适用于 class 组件。

首先如果父组件传入子组件的props不改变或者是引用的地址不变(浅比较),那么被memo包装过的函数组件就不会触发刷新,so,问题就真么愉快的解决了。

看下效果吧

结语

效果就是宁缺毋滥的,有时恰到好处的点缀,会起到意想不到的效果,内外兼修,才是最佳的实现,赶紧加上试试。

集成了该功能的🌰