분류 Reactjs

React Hooks를 사용하여 재사용 가능한 애니메이션 구성 요소를 작성하는 방법

컨텐츠 정보

  • 조회 438 (작성일 )

본문

How to build a reusable animation component using React Hooks 


애니메이션은 사용자를 기쁘게 합니다. 또한 React Hooks는 수많은 기사를 통해 개발자를 기쁘게 생각한다고 생각합니다. 그러나 나에게는 피로가 Hooks에 대한 나의 의견으로 들어 오기 시작했습니다.


그러나 평온은 나를 구했습니다. "새로운 방법"이 아니라 React Hooks에 적합한 예제를 찾았습니다.이 기사의 제목에서 짐작할 수 있듯이 이 예제는 애니메이션입니다.


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


jd-delete.2019-07-12-19_32_00.gif 


불행히도,이 작업을 수행하는 데 뉘앙스가 있습니다. 그리고 내 솔루션으로 React Hooks를 잘 사용했습니다.


우리 뭐 할까? 

  • 기본 예제 응용 프로그램으로 시작
  • 요소가 사라지는 점진적 애니메이션, 일부 도전 과제 강조
  • 원하는 애니메이션을 완성하면 재사용 가능한 애니메이션 구성 요소를 리팩터링합니다.
  • 이 구성 요소를 사용하여 사이드 바와 탐색 바에 애니메이션을 적용합니다.
  • 그리고…. (당신은 끝까지 읽고 / 점프해야합니다)

참을성이 없는 사람들을 위해 이 프로젝트의 코드에 대한 GitHub 저장소가 있습니다. 각 단계마다 태그가 있습니다. (각 태그에 대한 링크 및 설명은 README를 참조하십시오.)


베이스 라인 


create-react-app를 사용하여 간단한 응용 프로그램을 만들었습니다. 간단한 카드 그리드가 있습니다. 개별 카드를 숨길 수 있습니다.


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

(예, 위의 후크를 사용하고 있지만 흥미로운 후크 사용은 아닙니다.)


애니메이션 추가 


자체 애니메이션 라이브러리를 구축하는 대신 animate.css와 같은 애니메이션 라이브러리를 찾았습니다. react-animate.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는 불투명도 및 기타 CSS 속성에 애니메이션을 적용합니다. display 속성에서 CSS 전환을 수행 할 수 없습니다. 따라서 보이지 않는 개체가 남아 있으며 문서 흐름에서 공간을 차지합니다.

animate-holes.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를 사용하여 표시 속성을 설정하기 전에 끊김 / 일시 중지를 최소화합니다. 기본 설정은 문제입니다.)


그리고 그것은 우리에게 원하는 효과를 줄 것입니다.

animate-no-holes.gif 



재사용 가능한 구성 요소 작성 


여기서 멈출 수는 있지만 두 가지 문제가 있습니다.

  1. 이 효과를 재현하기 위해 애니메이션 블록, 스타일 및 기능을 복사 / 붙여 넣기하고 싶지 않습니다.
  2. Box 구성 요소는 여러 종류의 논리, 즉 우려 분리를 위반합니다. 특히 Box의 필수 기능은 내용이 포함 된 카드를 렌더링하는 것입니다. 그러나 애니메이션 세부 사항이 혼합되어 있습니다.

Class Component 


애니메이션 상태를 관리하기 위해 전통적인 React 클래스 구성 요소를 만들 수 있습니다. 가시성을 토글하고 display CSS 속성의 시간 제한을 설정합니다.

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 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 후크에는 미묘한 부분이 있습니다. 주로 부작용 (상태 변경, 비동기 함수 호출 등)을 위한 것입니다.이 경우 이전 표시 값을 기반으로 내부 noDisplay 부울을 설정합니다.


useEffect를 위해 dependencies 배열에 visible을 추가하면 visibleEvisfect 후크가 visible 값이 변경 될 때만 호출됩니다.


useEffect가 클래스 구성 요소 혼란보다 훨씬 나은 솔루션이라고 생각합니다. ?


Reusing the Component: Sidebars and Navbars 


누구나 사이드 바와 탐색 모음을 좋아합니다. 각각 하나를 추가하겠습니다.

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

final-multiple.gif 

reuse achieved


그러나 우리는 끝나지 않았습니다… 


우리는 여기서 멈출 수 있었습니다. 그러나 우려 분리에 대한 이전 의견과 마찬가지로 Box, Sidebar 또는 Navbar의 렌더링 방법에서 AnimatedVisibility 구성 요소를 혼합하지 않는 것이 좋습니다. (소량의 복제이기도 합니다.)


HOC를 만들 수 있습니다. (실제로 애니메이션 및 HOC, React에서 애니메이션 미세 상호 작용을 작성하는 방법에 대한 기사를 작성했습니다.) 그러나 HOC에는 일반적으로 상태 관리 때문에 클래스 구성 요소가 포함됩니다.


그러나 React Hooks를 사용하면 HOC (functional programming approach)를 작성할 수 있습니다.

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

내 작업을 홍보 할 위험이 있으므로 깨끗한 결과 코드를 선호합니다.


최종 결과의 샌드 박스는 다음과 같습니다.


https://codesandbox.io/embed/github/csepulv/animated-visibility/tree/master/ 


Now What? 


간단한 애니메이션의 경우 내가 설명하는 접근 방식이 효과적입니다. 더 복잡한 경우에는 react-motion과 같은 라이브러리를 사용합니다.


그러나 애니메이션과 별도로 React Hooks는 읽기 쉽고 간단한 코드를 생성 할 수 있는 기회를 제공합니다. 그러나 사고에는 조정이 있습니다. useEffect와 같은 후크가 모든 수명주기 방법을 대체하는 것은 아닙니다. 공부하고 실험해야 합니다.


useHooks.com과 같은 사이트와 react-use와 같은 라이브러리, 다양한 사용 사례를 위한 후크 모음을 살펴 보는 것이 좋습니다.