如何用React-Hooks构建可复用的动画组件

23次阅读

共计 7779 个字符,预计需要花费 20 分钟才能阅读完成。

原文:https://www.freecodecamp.org/news/animating-visibility-with-css-an-example-of-react-hooks/
译文:前端技术小哥

Restration Hooks 取悦了开发人员。但对我来说,我已经开始对 Hooks 感到疲劳。
React Hooks 的示例,而不仅仅是“新方法”。正如大家根据本文的标题所猜测的,这个示例是一个动画。但机缘巧合下我改变了我的看法。
我正在开发一个使用网格中的卡片的阵营应用程序。当一个项目被删除时,我想让它的退出动画化,就像这样。


(我的目标)

不巧的是,这其实和我预计的有些许细微差别。而我的解决方案让我很好地利用了 React Hooks。

我们要做什么?
从一个基线示例应用程序开始
逐渐增加消失的动画元素,把难点挑出来重点处理
一旦我们实现了所需的动画,我们将重构一个可重用的动画组件
我们将使用此组件使侧边栏和导航栏具有动画效果
以及 …(你需要读 / 跳到最后)

对于没有耐心的人,这里是这个项目的代码的 GitHub repo。每个步骤都有标签。(有关每个标记的链接和描述,请参见自述)。

基线
我用创建反应的应用内创建了一个简单的应用程序。它有一个简单的卡片网格。我们可以隐藏单独的卡片。

(没有动画 – 卡片消失得不连贯)

这个的代码是很基础的,结果也是无趣的。当用户单击眼图标按钮时,会我们更改卡片的 display 属性。

function Box({word}) {const color = colors[Math.floor(Math.random() * 9)];
  const [visible, setVisible] = useState(true);
  function hideMe() {setVisible(false);
  }
  let style = {borderColor: color, backgroundColor: color};
  if (!visible) style.display = "none";
  return (<div className="box" style={style}>
      {" "}
      <div className="center">{word}</div>{" "}
      <button className="button bottom-corner" onClick={hideMe}>
        {" "}
        <i className="center far fa-eye fa-lg" />{" "}
      </button>{" "}
    </div>
  );
}

(是的,在上面我使用了鱼钩,但这个用法并不有趣。)

添加动画
我没有建立自己的动画库,而是寻找类似 animate.css 的动画库.React 动画 -CSS 是一个很棒的动画库,它提供了一个围绕 animate.css 的包装。
npm install –save react-animated-css
把 animate.css 添加到 index.html 的

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.2/animate.css" />

上面在的 Box 组件中,我们将其渲染更改为

return (<Animated animationIn="zoomIn" animationOut="zoomOut" isVisible={visible}>
    <div className="box" style={style}>
      <div className="center">{word}</div>
      <button className="button bottom-corner" onClick={hideMe}>
        <i className="center far fa-eye fa-lg" />
      </button>
    </div>
  </Animated>
);

和我们中预期的不相符
但是 animate.css 设置可以 opacity 状语从句:其他的 CSS 属性的动画; 不能我们在 display 属性上进行 CSS 转换因此,一个不可见的对象仍然存在,它占用了文档流中的空间。


如果大家试着谷歌一下,就会发现有一些解决方案建议使用计时器在动画结束时设置 display: none。

所以我们可以补充一点

function Box({word}) {const color = colors[Math.floor(Math.random() * 9)];
  const [visible, setVisible] = useState(true);
  const [fading, setFading] = useState(false);

  function hideMe() {setFading(true);
    setTimeout(() => setVisible(false), 650);
  }

  let style = {borderColor: color, backgroundColor: color};

  return (
    <Animated
      animationIn="zoomIn"
      animationOut="zoomOut"
      isVisible={!fading}
      style={visible ? null : { display: "none"}}
    >
      <div className="box" style={style}>
        <div className="center">{word}</div>
        <button className="button bottom-corner" onClick={hideMe}>
          <i className="center far fa-eye fa-lg" />
        </button>
      </div>
    </Animated>
  );
}

(注意:默认动画持续时间为 1000 毫秒我使用 650 毫秒作为超时,以便在设置 display。属性之前最小化停滞感这是一个优先考虑的问题)

这将给我们带来预期中的效果。

(奈思!)

创建可复用组件
我们可以在这里停下来,但有两个问题(对我来说):
1、我不想复制 / 粘贴 Animated 块,样式和函数来重新创建此效果 2、Box 组件混合了不同类型的逻辑,即违反了关注点分离。具体来说,Box 的基本功能是使用卡片呈现其内容。但动画细目混杂在了一起。

类组件
我们可以创建一个传统的阵营类组件来管理动画状态:切换可见性并设置 displayCSS 属性的超时。

class AnimatedVisibility extends Component {constructor(props) {super(props);
    this.state = {noDisplay: false, visible: this.props.visible};
  }

  componentWillReceiveProps(nextProps, nextContext) {if (!nextProps.visible) {this.setState({ visible: false});
      setTimeout(() => this.setState({ noDisplay: true}), 650);
    }
  }

  render() {
    return (
      <Animated
        animationIn="zoomIn"
        animationOut="zoomOut"
        isVisible={this.state.visible}
        style={this.state.noDisplay ? { display: "none"} : null}
      >
        {this.props.children}
      </Animated>
    );
  }
}

然后用于

function Box({word}) {const color = colors[Math.floor(Math.random() * 9)];
  const [visible, setVisible] = useState(true);

  function hideMe() {setVisible(false);
  }

  let style = {borderColor: color, backgroundColor: color};

  return (<AnimatedVisibility visible={visible}>
      <div className="box" style={style}>
        <div className="center">{word}</div>
        <button className="button bottom-corner" onClick={hideMe}>
          <i className="center far fa-eye fa-lg" />
        </button>
      </div>
    </AnimatedVisibility>
  );
}

这的确创建了一个可复用组件,但它有点过于复杂。我们本可以做得更好。

React Hooks 和 useEffect
React Hooks 是 React 16.8 中的新功能。它们为 React 组件中的生命周期和状态管理提供了一种更简单的方法.Hook useEffect 为我们使用 componentWillReceiveProps 提供了一个精妙的替代品。代码变得更简单,我们可以再次使用功能组件。

function AnimatedVisibility({visible, children}) {const [noDisplay, setNoDisplay] = useState(!visible);
  useEffect(() => {if (!visible) setTimeout(() => setNoDisplay(true), 650);
    else setNoDisplay(false);
  }, [visible]);

  const style = noDisplay ? {display: "none"} : null;
  return (
    <Animated
      animationIn="zoomIn"
      animationOut="zoomOut"
      isVisible={visible}
      style={style}
    >
      {children}
    </Animated>
  );
}

使用 useEffect hook 有一些微妙之处。它主要用于副作用:更改状态,调用异步函数等。在我们的示例中,它根据先前的值可见设置内部 noDisplay 布尔值。通过向依赖关系数组的 useEffect 中添加 visible,我们的 useEffect 挂钩只会在 visible 值发生更改时调用。

我认为 useEffect 是一个比类组件杂波更好的解决方案,你们觉得呢?复用组件:侧边栏和导航栏

大家都喜欢侧边栏和导航栏。那我们分别加一个吧。

function ToggleButton({label, isOpen, onClick}) {
  const icon = isOpen ? (<i className="fas fa-toggle-off fa-lg" />) : (<i className="fas fa-toggle-on fa-lg" />);
  return (<button className="toggle" onClick={onClick}>
      {label} {icon}
    </button>
  );
}

function Navbar({open}) {
  return (
    <AnimatedVisibility
      visible={open}
      animationIn="slideInDown"
      animationOut="slideOutUp"
      animationInDuration={300}
      animationOutDuration={600}
    >
      <nav className="bar nav">
        <li>Item 1</li>
        <li>Item 2</li>
        <li>Item 3</li>
      </nav>
    </AnimatedVisibility>
  );
}

function Sidebar({open}) {
  return (
    <AnimatedVisibility
      visible={open}
      animationIn="slideInLeft"
      animationOut="slideOutLeft"
      animationInDuration={500}
      animationOutDuration={600}
      className="on-top"
    >
      <div className="sidebar">
        <ul>
          <li>Item 1</li>
          <li>Item 2</li>
          <li>Item 3</li>
        </ul>
      </div>
    </AnimatedVisibility>
  );
}

function App() {const [navIsOpen, setNavOpen] = useState(false);
  const [sidebarIsOpen, setSidebarOpen] = useState(false);

  function toggleNav() {setNavOpen(!navIsOpen);
  }

  function toggleSidebar() {setSidebarOpen(!sidebarIsOpen);
  }

  return (
    <Fragment>
      <main className="main">
        <header className="bar header">
          <ToggleButton
            label="Sidebar"
            isOpen={sidebarIsOpen}
            onClick={toggleSidebar}
          />
          <ToggleButton label="Navbar" isOpen={navIsOpen} onClick={toggleNav} />
        </header>
        <Navbar open={navIsOpen} />
        <Boxes />
      </main>
      <Sidebar open={sidebarIsOpen} />
    </Fragment>
  );
}

(达成复用)

但我们还没有结束 ……
我们可以在这里停下来。但正如我之前关于分离关注的评论一样,我宁愿避免在 Box,Sidebar 和 Navbar 的渲染方法中混合 AnimatedVisibility 组件。(这也是少量的重复。)

我们可以创建一个 HOC。(事实上,我曾写了一篇关于动画和 HOC 的文章,如何在 React 中构建动画微交互。)但是由于状态管理,HOC 通常涉及类组件。但是有了 React Hooks,我们可以编写 HOC(函数式编程方法)。

function AnimatedVisibility({
  visible,
  children,
  animationOutDuration,
  disappearOffset,
  ...rest
})
// ... same as before
}


function makeAnimated(
  Component,
  animationIn,
  animationOut,
  animationInDuration,
  animationOutDuration,
  disappearOffset
) {return function({ open, className, ...props}) {
    return (
      <AnimatedVisibility
        visible={open}
        animationIn={animationIn}
        animationOut={animationOut}
        animationInDuration={animationInDuration}
        animationOutDuration={animationOutDuration}
        disappearOffset={disappearOffset}
        className={className}
      >
        <Component {...props} />
      </AnimatedVisibility>
    );
  };
}

export function makeAnimationSlideLeft(Component) {return makeAnimated(Component, "slideInLeft", "slideOutLeft", 400, 500, 200);
}

export function makeAnimationSlideUpDown(Component) {return makeAnimated(Component, "slideInDown", "slideOutUp", 400, 500, 200);
}

export default AnimatedVisibility

在然后 App.js 中使用这些基于函数的 HOC

function Navbar() {
  return (
    <nav className="bar nav">
      <li>Item 1</li>
      <li>Item 2</li>
      <li>Item 3</li>
    </nav>
  );
}

function Sidebar() {
  return (
    <div className="sidebar">
      <ul>
        <li>Item 1</li>
        <li>Item 2</li>
        <li>Item 3</li>
      </ul>
    </div>
  );
}

const AnimatedSidebar = makeAnimationSlideLeft(Sidebar);
const AnimatedNavbar = makeAnimationSlideUpDown(Navbar);

function App() {const [navIsOpen, setNavOpen] = useState(false);
  const [sidebarIsOpen, setSidebarOpen] = useState(false);

  function toggleNav() {setNavOpen(!navIsOpen);
  }

  function toggleSidebar() {setSidebarOpen(!sidebarIsOpen);
  }

  return (
    <Fragment>
      <main className="main">
        <header className="bar header">
          <ToggleButton
            label="Sidebar"
            isOpen={sidebarIsOpen}
            onClick={toggleSidebar}
          />
          <ToggleButton label="Navbar" isOpen={navIsOpen} onClick={toggleNav} />
        </header>
          <AnimatedNavbar open={navIsOpen} />
        <Boxes />
      </main>
      <AnimatedSidebar open={sidebarIsOpen} className="on-top"/>
    </Fragment>
  );
}

比起冒着推广自己作品的风险,我更喜欢干净的结果代码。下面是最终结果的沙盒。(此处更改)

现在还需要做什么呢?
对于简单的动画,我上面提到的方法效果很好。对于更复杂的情况,我会使用像 react-motion 这样的库。但除了制作动画,React Hooks 提供了创建可读和简单代码的机会。但是,我们的思维需要调整。像 useEffect 这样的钩并不是所有生命周期方法的直接替代品。我们需要需要大量的学习和实验。

我建议大家可以查看像 useHooks.com 这样的网站和像反应使用这样的库,这是一个用于各种各样的用例的钩集合。

希望本文能帮助到您!
看之后
点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)
关注公众号「新前端社区」,享受文章首发体验!
每周重点攻克一个前端技术难点。

正文完
 0