概要
useCardAnimation
は、NetflixなどのUIなどでよく見られる カードUIの動き (ホバー/選択/アニメーション) を簡単に実現できるカスタムフックです。
本記事では、
「設計の意図」を体感できるように、基本的なコンポーネントから順を追って解説します。
本記事の構成
以下の4ステップで進みます。
- 基礎のデモ: Cardコンポーネント(まずは手動でUI制御)
- useCardAnimation の hooks ファイルを作成(動作ロジックの再利用化)
- useCardAnimation を使ったコンポーネントを作成(導入前の練習)
- useCardAnimation 導入デモ(実用レベルの導入コンポーネント)
実施条件
- ReactおよびTypeScriptの基本的な知識がある
- Vite または Create React App 等でReact環境が構築済み
- JSX/Props/Stateの扱いが理解できている
環境
ツール | バージョン | 目的 |
---|---|---|
Node.js | 22.5.1 | Reactアプリ実行環境 |
React | 19.1.0 | UIコンポーネントの構築 |
TypeScript | 4.9 | 型定義による安全な開発 |
fetch API | ブラウザ標準搭載 | API通信処理 |
CSS-in-JS | inline style利用 | アニメーションや動きの記述 |
基礎のデモ: Cardコンポーネント
まずは useCardAnimation
を使わない状態で、手動でカードの動きを実装します。
// importセクション
import React, { useState } from 'react';
// 型定義セクション
// (今回はPropsがないため省略)
// 関数定義セクション
const SimpleCard = () => {
// 内部状態管理セクション
const [hovered, setHovered] = useState(false);
const [selected, setSelected] = useState(false);
// イベントハンドラーセクション
const handleClick = () => setSelected((prev) => !prev);
// JSX構築セクション
const scale = selected ? 1.1 : hovered ? 1.05 : 1;
const zIndex = selected ? 11 : hovered ? 2 : 1;
return (
<div
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onClick={handleClick}
style={{
transform: `scale(${scale})`,
zIndex,
transition: 'transform 300ms ease, z-index 300ms ease',
cursor: 'pointer',
border: '1px solid gray',
padding: '1rem',
margin: '1rem',
}}
>
<p>{selected ? 'SelectedCard' : 'NotSelectedCard'}</p>
</div>
);
};
export default SimpleCard;
▶ このコードで理解すべきポイント:
- ホバーで
hovered
= true - クリックで
selected
= true / false -
scale
とzIndex
を分岐して表現を切り替えている
useCardAnimation の hooks ファイルを作成
このセクションでは、上記のロジックを共通化し、useCardAnimation
フックとして抽出します。コードの再利用性を高め、可読性を維持します。
// useCardAnimation.ts
// importセクション
import { useState, useEffect, useCallback, useRef } from 'react';
// 型定義セクション
export interface CardAnimationState {
isHovered: boolean;
isSelected: boolean;
isAnimating: boolean;
}
export interface CardAnimationHandlers {
onMouseEnter: () => void;
onMouseLeave: () => void;
onClick: () => void;
}
export interface UseCardAnimationReturn {
state: CardAnimationState;
handlers: CardAnimationHandlers;
style: React.CSSProperties;
className: string;
}
// カスタムフック定義セクション
export const useCardAnimation = () => {
// 内部状態管理セクション
const [isHovered, setIsHovered] = useState(false);
const [isSelected, setIsSelected] = useState(false);
const [isAnimating, setIsAnimating] = useState(false);
const animationRef = useRef<NodeJS.Timeout | null>(null);
// アニメーション開始
const startAnimation = useCallback(() => {
setIsAnimating(true);
if (animationRef.current) clearTimeout(animationRef.current);
animationRef.current = setTimeout(() => setIsAnimating(false), 300);
}, []);
// イベントハンドラーセクション
const onMouseEnter = useCallback(() => {
setIsHovered(true);
startAnimation();
}, [startAnimation]);
const onMouseLeave = useCallback(() => {
setIsHovered(false);
startAnimation();
}, [startAnimation]);
const onClick = useCallback(() => {
setIsSelected((prev) => !prev);
startAnimation();
}, [startAnimation]);
// 副作用処理セクション
useEffect(() => {
return () => {
if (animationRef.current) clearTimeout(animationRef.current);
};
}, []);
// JSX構築セクション(スタイル・クラス名)
const scale = isSelected ? 1.1 : isHovered ? 1.05 : 1;
const zIndex = isSelected ? 11 : isHovered ? 2 : 1;
const style: React.CSSProperties = {
transform: `scale(${scale})`,
zIndex,
transition: 'transform 300ms ease, z-index 300ms ease',
cursor: 'pointer',
transformOrigin: 'center',
};
const className = [
'animated-card',
isHovered && 'hovered',
isSelected && 'selected',
isAnimating && 'animating',
].filter(Boolean).join(' ');
return {
state: { isHovered, isSelected, isAnimating },
handlers: { onMouseEnter, onMouseLeave, onClick },
style,
className,
};
};
useCardAnimation を使ったコンポーネントを作成
このステップでは、作成した useCardAnimation
を活用して、実際のカードUIコンポーネントに導入してみましょう。
// CardWithAnimation.tsx
// importセクション
import React from 'react';
import { useCardAnimation } from './useCardAnimation';
// 型定義セクション
// Propsなしのため省略
// 関数定義セクション
const CardWithAnimation = () => {
// 内部状態管理セクション
const { state, handlers, style, className } = useCardAnimation();
// JSX構築セクション
return (
<div
{...handlers}
style={{
...style,
border: '1px solid gray',
padding: '1rem',
margin: '1rem',
}}
className={className}
>
<p>{state.isSelected ? 'SelectedCard' : 'NotSelectedCard'}</p>
</div>
);
};
export default CardWithAnimation;
useCardAnimation
導入デモ
最後に、上記の CardWithAnimation
を実際に使ってみる親コンポーネントの例です。
// AnimatedCardDemo.tsx
// importセクション
import React from 'react';
import CardWithAnimation from './CardWithAnimation';
// 型定義セクション
// Propsなしのため省略
// 関数定義セクション
const AnimatedCardDemo = () => {
// JSX構築セクション
return (
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
<CardWithAnimation />
<CardWithAnimation />
<CardWithAnimation />
</div>
);
};
export default AnimatedCardDemo;
描画される結果
- カードをホバー: 拡大(hoverScale)
- クリック: さらに拡大(selectedScale)& z-index 切替
- 再クリック: 状態リセット
まとめ
useCardAnimation
を理解するには、まずはカード動作のロジックを手動で実装してみることが最適です。
その上で、カード表示コンポーネントの数が増えたり、動作の再利用性を高めたくなったら useCardAnimation
の形で分離するのが最適です。
このような段階的理解を通じて、「カスタムフックとは何か」「どう再利用性を高めるか」 を自然に身につけることができます。