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

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 的,使用网格布局组合卡片组件的应用,当删除某个卡片组件时,为它添加动画效果,看起来像下面一样:

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

我们将要做什么?

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

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

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

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

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

骨架

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

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

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

添加动画

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

安装 react-animated-css

npm install --save react-animated-css

在  index.html  中添加 animate.css


在上面的  Box  组件中,将渲染结果改为

return (
  
    
{word}
);

完全是我们想要的东西

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 (
    
      
{word}
); }

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

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

构建一个可复用的组件

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

  • 我不想复制/粘贴  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 (
      
        {this.props.children}
      
    );
  }
}

然后使用它

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 (
    
      
{word}
); }

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

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 (
    
      {children}
    
  );
}

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

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

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

组件复用: Sidebars 和 Navbars

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

function ToggleButton({ label, isOpen, onClick }) {
  const icon = isOpen ? (
    
  ) : (
    
  );
  return (
    
  );
}

function Navbar({ open }) {
  return (
    
      
    
  );
}

function Sidebar({ open }) {
  return (
    
      
  • Item 1
  • Item 2
  • Item 3
); } function App() { const [navIsOpen, setNavOpen] = useState(false); const [sidebarIsOpen, setSidebarOpen] = useState(false); function toggleNav() { setNavOpen(!navIsOpen); } function toggleSidebar() { setSidebarOpen(!sidebarIsOpen); } return (
); }

还没结束…

到这里我们就可以停下了,但就像我之前提到的关注点分离,我更倾向于避免在  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 (
      
        
      
    );
  };
}

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

function Sidebar() {
  return (
    
  • Item 1
  • Item 2
  • Item 3
); } 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 (
); }

接下来呢?

对于简单的动画,可以使用我所提到的方法。如果比较复杂,我会使用像  react-motion  这样的库。

不仅仅是动画,React Hooks 让我们可以编写可读性高、更简洁的代码。 但是,我们需要在思维上有个调整,像  useEffect 这样的 Hooks 不完全是 React 生命周期函数的替代品,你需要深入学习和研究。

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

Happy coding!

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

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