[譯] 如何用React Hooks構建可複用的動畫組件

原文: www.freecodecamp.org/news...
譯文:前端技術小哥css

Restration Hooks取悅了開發人員。但對我來講,我已經開始對Hooks 感到疲勞。 React Hooks的示例,而不只僅是「新方法」。正如你們根據本文的標題所猜想的,這個示例是一個動畫。但機緣巧合下我改變了個人見解。 我正在開發一個使用網格中的卡片的陣營應用程序。當一個項目被刪除時,我想讓它的退出動畫化,就像這樣。html

(個人目標)

不巧的是,這其實和我預計的有些許細微差異。而個人解決方案讓我很好地利用了React Hooks。前端

咱們要作什麼?
從一個基線示例應用程序開始
逐漸增長消失的動畫元素,把難點挑出來重點處理
一旦咱們實現了所需的動畫,咱們將重構一個可重用的動畫組件
咱們將使用此組件使側邊欄和導航欄具備動畫效果
以及...(你須要讀/跳到最後)react

對於沒有耐心的人,這裏是這個項目的代碼的GitHub repo。每一個步驟都有標籤。(有關每一個標記的連接和描述,請參見自述)。ajax

基線
我用建立反應的應用內建立了一個簡單的應用程序。它有一個簡單的卡片網格。咱們能夠隱藏單獨的卡片。npm

(沒有動畫 - 卡片消失得不連貫)編程

這個的代碼是很基礎的,結果也是無趣的。當用戶單擊眼圖標按鈕時,會咱們更改卡片的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>
  );
}
複製代碼

(是的,在上面我使用了魚鉤,但這個用法並不有趣。)bash

添加動畫
我沒有創建本身的動畫庫,而是尋找相似animate.css的動畫庫.React動畫-CSS是一個很棒的動畫庫,它提供了一個圍繞animate.css的包裝。
npm install --save react-animated-css
把animate.css添加到index.html的app

<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。屬性以前最小化停滯感這是一個優先考慮的問題)

這將給咱們帶來預期中的效果。

(奈思!)

建立可複用組件
咱們能夠在這裏停下來,但有兩個問題(對我來講):
一、我不想複製/粘貼Animated塊,樣式和函數來從新建立此效果 二、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這樣的網站和像反應使用這樣的庫,這是一個用於各類各樣的用例的鉤集合。

但願本文能幫助到您!
看以後
點贊,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓-_-)
關注公衆號「新前端社區」,享受文章首發體驗!
每週重點攻克一個前端技術難點。

相關文章
相關標籤/搜索