React でカルーセルを自作してみた


以下の記事を参考に、React でカルーセルを自作してみました。

https://qiita.com/wintyo/items/a37a197f69aa205297a5

完成したものがこちらになります。

<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="WNyaroW" data-user="wataru86" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/wataru86/pen/WNyaroW">
  @emotion/react in CodePen</a> by Wataru Yamada (<a href="https://codepen.io/wataru86">@wataru86</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

# 実装までの流れ

完成したプログラムを CodePen で表示したかったので、最初に CodePen で TypeScript + React + `@emotion/react` が使える環境を作りました。
`@emotion/react` を CodePen で使用できるようにするまで試行錯誤したときの記事はこちらになります。

https://qiita.com/wataru86/items/17d5a33172cf4feebaef


参考にした記事に書いてある通り、親要素に `white-space: nowrap`を、子要素に `display: inline-block` を付けました。


```react
const App = () => {
  return (
    <div css={screenStyle}>
      <div css={itemContainerStyle}>
        <div css={itemStyle}>1</div>
        <div css={itemStyle}>2</div>
        <div css={itemStyle}>3</div>
        <div css={itemStyle}>4</div>
        <div css={itemStyle}>5</div>
      </div>
    </div>
  )
}

const screenStyle = css({
  width: 200,
  overflow: "hidden",
})

const itemContainerStyle =
  css({
    width: 100,
    height: 100,
    whiteSpace: "nowrap",
    margin: "auto",
  })

const itemStyle = css({
  width: "100%",
  height: "100%",
  display: "inline-block",
})
```

これで、あとは `itemContainerStyle` に `transition` と `transform: translate3d` を追加して、変数で移動する距離を計算すればカルーセルになります。

`useState` で中央に表示する item の番号の状態を持って、 `button` で増減させれば左右に動かせます。

```tsx
const App = () => {
  const [currentItem, setCurrentItem] = useState(0)
  const handlePrev = () => setCurrentItem((n) => n - 1)
  const handleNext = () => setCurrentItem((n) => n + 1)
  return (
    <>
      <div css={screenStyle}>
        <div css={itemContainerStyle}>
          <div css={itemStyle}>1</div>
          <div css={itemStyle}>2</div>
          <div css={itemStyle}>3</div>
          <div css={itemStyle}>4</div>
          <div css={itemStyle}>5</div>
        </div>
      </div>
      <div>
        <button onClick={handlePrev}>prev</button>
        <button onClick={handleNext}>next</button>
        <button onClick={toggleColor}>toggle color</button>
      </div>
    </>
  )
}


const itemContainerStyle = (n: number) =>
  css({
    width: 100,
    height: 100,
    whiteSpace: "nowrap",
    margin: "auto",
    transform: `translate3d(${-100 * n}%, 0, 0)`,
    ...(isTransition ? { transition: "transform 1s" } : {}),
  })
```

無限カルーセルの方法もこちらの記事に書いてあったので、

https://qiita.com/wintyo/items/a37a197f69aa205297a5#%E7%84%A1%E9%99%90%E3%82%AB%E3%83%AB%E3%83%BC%E3%82%BB%E3%83%AB%E3%81%AE%E5%AE%9F%E8%A3%85

方法を簡単に説明すると、以下を追加します。


- リストの前後に要素のコピーを追加する
- コピーした要素への遷移が終わったときに、アニメーションなしでコピー元の要素に移動する

元のリストはこれで、

```
1 2 3 4 5
```

コピー後はこうなります。

```
4' 5' 1 2 3 4 5 1' 2'
^^ ^^           ^^ ^^   <- コピーした要素
```

`5` から `1'` にアニメーションした後に、アニメーションなしで `1` に移動すれば無限に回すことができます。

実装としては、新たな状態 `isTransition` を一つ用意して、これが `true` のときにアニメーションして、これが `false` の時はアニメーションなしで移動させます。

```tsx
  const [isTransition, setIsTransition] = useState(false)
```

そして、`onTransitionEnd` で `isTransition` を変化させてコピーした要素から元の要素へ移動を行えば完成です。

```tsx
        <div
          css={itemContainerStyle(currentItem, isTransition)}
          onTransitionEnd={handleTransitionEnd}
        >
```

以下の例の `toggle color` ボタンで、コピーした要素と元の要素の色を分けたり、同じにしたりできるので、試してみてください！

<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="WNyaroW" data-user="wataru86" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/wataru86/pen/WNyaroW">
  @emotion/react in CodePen</a> by Wataru Yamada (<a href="https://codepen.io/wataru86">@wataru86</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

詳しいロジックの説明は元の記事に記載してあるのでそちらを参考にしてください。