0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[React][Hooks] Custom Hooks `useCardAnimation` の使い方

0
Last updated at Posted at 2025-08-04

概要

  • Reactアプリケーションで、カードUIのホバーや選択時のアニメーション挙動を簡潔に実装するための useCardAnimation カスタムフックを紹介
  • 再利用可能な構造にすることで、複数のカードUIに共通のアニメーションを適用可能
  • Netflix や Spotify のような UIに近づけたい場合に活用でき、コードの見通し・保守性も高まります

実施条件

  • React + TypeScript プロジェクトが構築済みであること
  • カードUIを複数作成し、動きを付けたい要件があること
  • 状態管理・イベントハンドリングに対してReact Hooksの基礎理解があること

環境

ツール バージョン 目的
Node.js 22.5.1 Reactアプリ実行環境
React 19.1.0 UI構築
TypeScript 4.9 型定義による安全な開発
CSS-in-JS inline style使用 アニメーション表現用

型定義セクション

// 型定義セクション
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;
}

useCardAnimationカスタムフックの基本構造

  1. importセクション
  2. 型定義セクション
  3. カスタムフック定義セクション
    3.1 内部状態管理セクション
    3.2 アニメーションセクション
    3.3 イベントハンドラーセクション
    3.4 副作用処理セクション
    3.5 返り値構築・ロジックセクション

基本構文: useCardAnimation.ts

// 1. importセクション
import { useState, useRef, useCallback, useEffect } from 'react';

// 2. 型定義セクション
// (上記参照)

// 3. カスタムフック定義セクション
export const useCardAnimation = (): UseCardAnimationReturn => {
  // 3.1 内部状態管理セクション
  const [isHovered, setIsHovered] = useState(false);
  const [isSelected, setIsSelected] = useState(false);
  const [isAnimating, setIsAnimating] = useState(false);
  const timerRef = useRef<NodeJS.Timeout | null>(null);
  
  // 3.2 アニメーションセクション
  const triggerAnimation = useCallback(() => {
    setIsAnimating(true);
    if (timerRef.current) clearTimeout(timerRef.current);
    timerRef.current = setTimeout(() => setIsAnimating(false), 300);
  }, []);

  // 3.3 イベントハンドラーセクション
  const onMouseEnter = useCallback(() => {
    setIsHovered(true);
    triggerAnimation();
  }, [triggerAnimation]);

  const onMouseLeave = useCallback(() => {
    setIsHovered(false);
    triggerAnimation();
  }, [triggerAnimation]);

  const onClick = useCallback(() => {
    setIsSelected((prev) => !prev);
    triggerAnimation();
  }, [triggerAnimation]);

  // 3.4 副作用処理セクション
  useEffect(() => {
    return () => {
      if (timerRef.current) clearTimeout(timerRef.current);
    };
  }, []);

  // 3.5 返り値構築・ロジックセクション
  const scale = isSelected ? 1.1 : isHovered ? 1.05 : 1;
  const zIndex = isSelected ? 10 : 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 = [
    'card',
    isHovered && 'hovered',
    isSelected && 'selected',
    isAnimating && 'animating',
  ].filter(Boolean).join(' ');

  return {
    state: { isHovered, isSelected, isAnimating },
    handlers: { onMouseEnter, onMouseLeave, onClick },
    style,
    className,
  };
};

活用例

1. 基本カード:選択状態を切り替えるだけのカードUI

// 1. importセクション
import React from 'react';
import { useCardAnimation } from './useCardAnimation';

// 2. 型定義セクション(省略)

// 3. 関数定義セクション
const CardBasic = () => {
  // 3.1 状態管理セクション
  const { handlers, style, className, state } = useCardAnimation();

  // 3.2 イベントハンドラーセクション(hook内)

  // 3.3 JSX構築セクション
  return (
    <div
      {...handlers}
      style={{ ...style, border: '1px solid gray', margin: '1rem', padding: '1rem' }}
      className={className}
    >
      <p>{state.isSelected ? '選択済みカード' : '未選択カード'}</p>
    </div>
  );
};

export default CardBasic;

2. 画像ギャラリー:複数の画像カードにホバー拡大アニメーションを適用

// 1. importセクション
import React from 'react';
import { useCardAnimation } from './useCardAnimation';

// 2. 型定義セクション
interface ImageCardProps {
  src: string;
}

// 3. 関数定義セクション
const ImageCard = ({ src }: ImageCardProps) => {
  // 3.1 状態管理セクション
  const { handlers, style, className } = useCardAnimation();

  // 3.3 JSX構築セクション
  return (
    <div
      {...handlers}
      style={{ ...style, borderRadius: '8px', overflow: 'hidden', margin: '1rem' }}
      className={className}
    >
      <img src={src} alt="card" style={{ width: '200px', height: 'auto', display: 'block' }} />
    </div>
  );
};

const ImageCardGallery = () => {
  // 3.1 状態管理セクション
  const images = ['/img1.jpg', '/img2.jpg', '/img3.jpg'];

  // 3.3 JSX構築セクション
  return (
    <div style={{ display: 'flex' }}>
      {images.map((src, i) => <ImageCard key={i} src={src} />)}
    </div>
  );
};

export default ImageCardGallery;

3. ダッシュボード:各カードがホバー・選択可能なダッシュボードUI

// 1. importセクション
import React from 'react';
import { useCardAnimation } from './useCardAnimation';

// 2. 型定義セクション
interface DashboardCardProps {
  title: string;
  icon: string;
}

// 3. 関数定義セクション
const DashboardCard = ({ title, icon }: DashboardCardProps) => {
  // 3.1 状態管理セクション
  const { handlers, style, state } = useCardAnimation();

  // 3.3 JSX構築セクション
  return (
    <div
      {...handlers}
      style={{
        ...style,
        padding: '1.5rem',
        backgroundColor: state.isSelected ? '#333' : '#444',
        color: '#fff',
        borderRadius: '10px',
        width: '150px',
        textAlign: 'center',
        margin: '1rem',
      }}
    >
      <div style={{ fontSize: '2rem' }}>{icon}</div>
      <p>{title}</p>
    </div>
  );
};

const Dashboard = () => {
  // 3.1 状態管理セクション
  const items = [
    { title: 'Portfolio', icon: '📊' },
    { title: 'Trade', icon: '💹' },
    { title: 'Settings', icon: '⚙️' },
  ];

  // 3.3 JSX構築セクション
  return (
    <div style={{ display: 'flex', justifyContent: 'center' }}>
      {items.map((item, i) => <DashboardCard key={i} {...item} />)}
    </div>
  );
};

export default Dashboard;

カードアニメーションの処理の流れ

  1. ユーザー操作(hover/click)でイベント発火
  2. カスタムフック内で setState により状態を更新
  3. アニメーション用の transformzIndex を変更
  4. 変更された style / className を通じてUIに反映

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?