20

使用 React Hooks 创建可复用的动画组件

 4 years ago
source link: https://mp.weixin.qq.com/s/rqivGGmQlg4-cECWIvVNfw
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

MrMnuyr.jpg!web

Photo by  Dylan Ferreira  on  Unsplash

原文: https://www.freecodecamp.org/news/animating-visibility-with-css-an-example-of-react-hooks/

作者: Christian Sepulveda

译者: Zou Li

提示: 文中的蓝色字体可点击“阅读原文”访问更多内容

动画总是会取悦用户。 看到各种文章的介绍,你可能会觉得开发者们喜欢使用 React Hooks,但我发现自己开始慢慢对 Hooks 产生厌倦了。

某个意外的发现让我对 React Hooks 有了新的认识,它不仅仅是一种新的开发方式。 也许你已经从文章标题猜到是什么了,没错,就是动画!

我正在开发一个基于 React 的,使用网格布局组合卡片组件的应用,当删除某个卡片组件时,为它添加动画效果,看起来像下面一样:

UnaMNnu.gif

但是,和图中效果相比较始终还是有点细微差别。 在我的接下来的解决方案中,很好地利用了 React Hooks。

我们将要做什么?

  • 开始构建一个基本的项目骨架

  • 为元素的消失添加动画效果,解决一些小问题

  • 最终效果实现后,将其重构为一个可复用的动画组件

  • 在顶部导航和侧边导航中使用该动画组件

如果你没耐心,这里有整个项目的 仓库地址 ,每一步都有相应的标记(链接地址和描述参考 README 文件)。

骨架

我使用  create-react-app  创建了一个简单的应用程序,它是一个简单的卡片网格结构,每个单独卡片可以被隐藏。

RV7FBrm.gif

实现代码很简单,效果也很无趣。 当用户点击眼睛图标时,我们改变卡片的  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>
  );
}

上面的代码中使用到了 React Hooks,但这不是 Hooks 最有趣的用途。

添加动画

我没有构建自己的动画库,而是使用了一个像  animate.css  这样的动画库。 react-animated-css  是一个很好的库,它为 animate.css 提供了一个包装器。

安装 react-animated-css

npm install --save react-animated-css

在  index.html  中添加 animate.css

<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 过渡效果,所以将卡片隐藏后,它始终在文档流中占据着位置。

fiAFbmY.gif

如果你搜索一下, 有些解决方案 是建议使用定时器在动画结束时设置  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>
  );
}

注意: 默认的动画时长是 1000ms,我使用的是 650ms,为了在设置  display  属性之前减少卡顿/暂停现象(这只是个人喜好)。

这样我们就能得到想要的效果。

Uny2QfQ.gif

构建一个可复用的组件

现在到此为止,但目前有两个问题(对于我来说)

  • 我不想复制/粘贴  Animated  代码块,样式,功能,来重复实现相同效果。

  • Box  组件混合了不同类型的逻辑,例如: 违反了关注点分离的概念。 准确的说, Box  的主要功能是渲染卡片内容,但是动画细节混入了。

类组件

我们可以创建一个传统的 React 类组件来管理和动画相关的状态: 切换隐藏/显示,设置  display  属性的超时时间。

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>
  );
}

这就实现了一个可复用的组件,但是还有点复杂,我们还可以优化一下。

Re act Hooks and useEffect

React Hooks  是 React 16.8 中的新特性,它们为 React 组件的生命周期和状态管理提供了一种更简单的方法

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  钩子还是有点不一样,它的主要目的是副作用: 改变状态,调用异步函数等等。 在我们的例子中,它根据之前的  visible  的值修改了内部的  noDisplay  布尔值。

将  visible  作为依赖添加到  useEffect  的依赖数组中,当  visible  的值发生变化时,  useEffect  钩子才会被调用。

和类组件的杂乱相比较,我认为  useEffect  是一种更好的解决方案。

组件复用: Sidebars 和 Navbars

大家都喜欢 Sidebar 和 Navbar,我们来添加一个吧。

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>
  );
}

2quYNne.gif还没结束...

到这里我们就可以停下了,但就像我之前提到的关注点分离,我更倾向于避免在  Box Sidebar  和  Navbar  的 render 方法中混合  AnimatedVisibility  组件(代码有点重复)。

我们可以创建一个高阶组件(HOC)。 由于状态管理的原因, HOCs 通常会涉及到类组件。

但是使用了 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 中使用这些基于函数式的 HOCs

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 这样的 Hooks 不完全是 React 生命周期函数的替代品,你需要深入学习和研究。

我建议看看像  useHooks.com  这样的网站,还有像  react-use  这样的库(不同钩子用例的集合)。

Happy coding!

Qfamu2Q.jpg!web

非营利组织 freeCodeCamp.org 自 2014 年成立以来,以“帮助人们免费学习编程”为使命,创建了大量免费的 编程教程,包括交互式课程、视频课程、文章等。线下开发者社区遍布 160 多个国家、2000 多个城市。 我们正在帮助全球数百万人学习编程,希望让世界上每个人都有机会获得免费的优质的编程教育资源,成为开发者或者运用编程去解决问题。

你也想成为 freeCodeCamp 社区的贡献者吗?欢迎了解  招募丨freeCodeCamp 翻译计划


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK