React / ChakraUI / framer-motion でいいねボタンアニメーション実装

## はじめに
フロントエンド学習歴半年未満の初心者です。現在ポートフォリオを作成していますが
Reactにおいて[Framer Motion](https://www.framer.com/motion/)というライブラリを知ったので使ってみます。
宣言的な構文が特徴で、少ないコード量でリッチなアニメーションが簡単に実装できます。
今回の様に部分的にさりげなく拘ったアニメーションを追加したい場合などに向いているのかなと思いました。

## この記事の対象読者
ReactとChakra UIを使っていて、アニメーションを実装したい方。

## 何を作るのか
おしゃれないいねボタンアニメーションを実装します。（できるとは言っていない）
イメージとしては以下のような、気持ちの良い感じのアニメーションが理想です。

**Twitter**
![d4lbw-djmtl (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682983/1463cbc9-fc43-2836-35f5-87a6d93d12a1.gif)
**Zenn**
![vros9-pz9l1 (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682983/ede60a76-16bc-bb5b-102a-f6577f81fa54.gif)

## 準備
```
yarn add @chakra-ui/react framer-motion@3.10.6 react-icons
```
※[Chakra UIが使える様になるまでの設定](https://chakra-ui.com/docs/getting-started)は割愛します

**追記**
---

https://github.com/chakra-ui/chakra-ui/issues/3618
framer-motionの最新版v4では本番環境で動作が不安定なようです。
`v3.10.6` で動作が確認できているとのことなのでこちらをインストールします。

---


react-iconsを使い、後ほど２つのハートマークアイコンをimportします。
（枠線のみのハートマークと塗り潰されたハートマーク）

### 試しに動かしてみる
[公式リポジトリ](https://github.com/framer/motion)にあるQuick Startを参考にとりあえず動かしてみました。

```tsx
import { motion } from 'framer-motion'
import type { VFC } from 'react'
import { useState } from 'react'

const LikeButtonWithCount: VFC = () => {
  const [isVisible, setIsVisible] = useState(true)

  const toggleVisible = () => {
    setIsVisible(!isVisible)
  }

  return (
    <motion.h1 animate={{ opacity: isVisible ? 1 : 0 }} onClick={toggleVisible}>
      hogehogehoge
    </motion.h1>
  )
}
```
![demooo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682983/f6a3f379-58f6-28e1-d1d8-81cf4d502e69.gif)

このように、とても簡単にアニメーションが実装できそうです🤔


## 実装

上述した２つのハートマークのアイコン遷移にアニメーションを追加していきたいと思います。
まずはアニメーションのない実装から。
コードが綺麗でない点については何卒ご容赦ください。


```tsx
import { Box, Icon, Text, Tooltip } from '@chakra-ui/react'
import type { VFC } from 'react'
import { useState } from 'react'
// AiFillHeart => ❤︎
// AiOutlineHeart => ♡
import { AiFillHeart, AiOutlineHeart } from 'react-icons/ai'

export type LikeButtonWithCountProps = {
  count: number
  isLiked: boolean
}

const LikeButtonWithCount: VFC<LikeButtonWithCountProps> = (props: LikeButtonWithCountProps) => {
  const [isLike, setIsLike] = useState(false)

  const toggleLike = () => {
    setIsLike(!isLike)
  }

  return (
    <Box display="flex" alignItems="center" color="gray.500">
      <Tooltip label="いいね！" bg="gray.400" fontSize="11px">
        <Text cursor="pointer" onClick={toggleLike}>
          <Icon
            as={isLike ? AiFillHeart : AiOutlineHeart}
            mr="2.5"
            fontSize="22px"
            color={isLike ? 'red.400' : ''}
          />
        </Text>
      </Tooltip>
      <Text>{props.count}</Text>
    </Box>
  )
}

export { LikeButtonWithCount }
```
![4l8au-35ozl (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682983/d948ea67-88ec-759a-dcb8-c20fd3bf341b.gif)
これでとりあえず動作するものになりました。
しかしやはり味気なさを感じてしまいますので改修していきます。


```tsx
import type { HTMLChakraProps } from '@chakra-ui/react'
import { Box, chakra, Icon, Text, Tooltip } from '@chakra-ui/react'
import type { HTMLMotionProps } from 'framer-motion'
import { motion, useAnimation } from 'framer-motion'
import type { VFC } from 'react'
import { useState } from 'react'
import { AiFillHeart, AiOutlineHeart } from 'react-icons/ai'

export type LikeButtonWithCountProps = {
  count: number
  isLiked: boolean
}

const LikeButtonWithCount: VFC<LikeButtonWithCountProps> = (props: LikeButtonWithCountProps) => {
  const [isLike, setIsLike] = useState(false)
  const controls = useAnimation()

  const toggleLike = async () => {
    await setIsLike(!isLike)
    controls.start({ scale: [1, 1.3, 1.6, 1.3, 1] })
  }

  type Merge<P, T> = Omit<P, keyof T> & T
  type MotionBoxProps = Merge<HTMLChakraProps<'div'>, HTMLMotionProps<'div'>>

  const MotionBox: React.FC<MotionBoxProps> = motion(chakra.div)

  return (
    <Box display="flex" alignItems="center" color="gray.500">
      <Tooltip label="いいね！" bg="gray.400" fontSize="11px">
        <MotionBox
          cursor="pointer"
          onClick={toggleLike}
          animate={controls}
          transition={{ duration: 0.2 }}
        >
          <Icon
            as={isLike ? AiFillHeart : AiOutlineHeart}
            mr="2.5"
            fontSize="22px"
            color={isLike ? 'red.400' : ''}
          />
        </MotionBox>
      </Tooltip>
      <Text>{props.count}</Text>
    </Box>
  )
}

export { LikeButtonWithCount }

```
![44 (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682983/4b306706-c930-da58-423a-171ff4620916.gif)

少し良くなりました。解説していきます。

```tsx
type Merge<P, T> = Omit<P, keyof T> & T
type MotionBoxProps = Merge<HTMLChakraProps<'div'>, HTMLMotionProps<'div'>>

const MotionBox: React.FC<MotionBoxProps> = motion(chakra.div)
```

こちらはFramerMotionの用意しているdiv要素の型と
Chakra UIの用意しているdiv要素の型をマージしています。
これによって`MotionBox`タグではFramer MotionとChakra UIのプロパティが
両方使えるという訳です。

ハートのアイコンを`MotionBox`で囲み、`onClick`,`animate`,`transition`プロパティを
指定しました。 さらに`useAnimation`Hooksをimportして
ボタンクリックをトリガーとしてアニメーションが発火するようにします。

```tsx
const toggleLike = async () => {
  await setIsLike(!isLike)
  (!isLike) {
    controls.start({ scale: [1, 1.3, 1.6, 1.3, 1] })
  }
}
```
クリック後のこちらの関数、未いいねならいいねをつける、いいね済ならいいねを外す
という処理ですが`setIsLike`をawaitさせないと
後続のアニメーションスタート処理がうまく動きません。
ここら辺は勉強中なのですが、この方法でもしかしたら何らかの弊害があるかもしれません...

`controls.start`にてアニメーションをスタートさせます。
配列でプロパティを渡すことでkeyframesと同じ役割をします。
`transition={{ duration: 0.2 }}`を指定している為、0.2秒
を５で割った地点でそれぞれの変更がアニメーションされます。

**最後にアニメーション部分を修正しました。**

```tsx
 controls.start({
   scale: [0.9, 1.1, 1.2, 1.2, 1],
   color: ['#FFF5F5', '#FED7D7', '#FEB2B2', '#FC8181', '#F56565'],
})
```

![j165v-llims (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682983/bdf8f51c-19db-e446-3cee-428af3239ecc.gif)
(あまり変わっていない)
本当はオブジェクトを周りに散らしたりしたかったのですが、
上手くいかなかったので一旦諦めます... 気が向いたらまたチャレンジするかもしれません。

## さいごに
https://www.framer.com/api/motion/
アニメーションに関しては本当にいろいろなことが出来る様です。
リッチな静的サイトを制作する機会があったら、また触ってみたいと思います。