Skip to content

Basic Syntax

Makoto Horikawa edited this page Jul 9, 2025 · 1 revision

📝 基本文法ガイド

GitHub Power Scouterで使用されているReact + TypeScriptの基本文法を、初心者にも分かりやすく実際のコードサンプルと共に解説します。

🎯 React基本概念

🧩 コンポーネントとは

// 関数コンポーネント(推奨)
const UserInput: React.FC = () => {
  return (
    <div>
      <input type="text" placeholder="GitHub Username" />
      <button>SCAN</button>
    </div>
  );
};

// TypeScript型定義付きコンポーネント
interface UserInputProps {
  onScan: (username: string) => void;
  isScanning: boolean;
}

const UserInput: React.FC<UserInputProps> = ({ onScan, isScanning }) => {
  return (
    <div>
      <input type="text" placeholder="GitHub Username" />
      <button disabled={isScanning} onClick={() => onScan('username')}>
        {isScanning ? 'SCANNING...' : 'SCAN'}
      </button>
    </div>
  );
};

🎛️ JSX基本記法

// JSX基本構文
const ScouterDisplay = () => {
  const powerLevel = 9001;
  const isScanning = true;
  
  return (
    <div className="scouter-display">
      {/* コメント記法 */}
      <h1>Power Level: {powerLevel}</h1>
      
      {/* 条件分岐 */}
      {isScanning ? (
        <p>Scanning in progress...</p>
      ) : (
        <p>Scan complete!</p>
      )}
      
      {/* 配列のレンダリング */}
      {['JavaScript', 'TypeScript', 'React'].map(skill => (
        <span key={skill} className="skill-badge">
          {skill}
        </span>
      ))}
      
      {/* イベントハンドラー */}
      <button onClick={() => console.log('Clicked!')}>
        Click me
      </button>
    </div>
  );
};

🪝 React Hooks

📊 useState - 状態管理

import { useState } from 'react';

const UserInput = () => {
  // 基本的な状態
  const [username, setUsername] = useState('');
  const [isScanning, setIsScanning] = useState(false);
  
  // 複合的な状態
  const [user, setUser] = useState<{
    name: string;
    followers: number;
  } | null>(null);
  
  // 配列の状態
  const [languages, setLanguages] = useState<string[]>([]);
  
  const handleScan = () => {
    setIsScanning(true);
    
    // 非同期処理の例
    setTimeout(() => {
      setUser({
        name: 'Test User',
        followers: 1000,
      });
      setLanguages(['JavaScript', 'TypeScript']);
      setIsScanning(false);
    }, 2000);
  };
  
  return (
    <div>
      <input
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        placeholder="Enter username"
      />
      <button onClick={handleScan} disabled={isScanning}>
        {isScanning ? 'Scanning...' : 'Scan'}
      </button>
      
      {user && (
        <div>
          <h3>{user.name}</h3>
          <p>Followers: {user.followers}</p>
          <ul>
            {languages.map(lang => (
              <li key={lang}>{lang}</li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
};

🎭 useEffect - 副作用処理

import { useState, useEffect } from 'react';

const ScouterDisplay = ({ username }: { username: string }) => {
  const [userData, setUserData] = useState(null);
  const [loading, setLoading] = useState(false);
  
  // コンポーネントマウント時の処理
  useEffect(() => {
    console.log('Component mounted');
    
    // クリーンアップ関数
    return () => {
      console.log('Component unmounted');
    };
  }, []); // 空の依存配列 = マウント時のみ実行
  
  // 特定の値が変更された時の処理
  useEffect(() => {
    if (!username) return;
    
    setLoading(true);
    
    const fetchUser = async () => {
      try {
        const response = await fetch(`/api/users/${username}`);
        const data = await response.json();
        setUserData(data);
      } catch (error) {
        console.error('Error fetching user:', error);
      } finally {
        setLoading(false);
      }
    };
    
    fetchUser();
  }, [username]); // username が変更される度に実行
  
  // タイマーの例
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Timer tick');
    }, 1000);
    
    return () => clearInterval(timer); // クリーンアップ
  }, []);
  
  return (
    <div>
      {loading ? (
        <p>Loading...</p>
      ) : (
        <div>{userData && <p>User: {userData.name}</p>}</div>
      )}
    </div>
  );
};

🧠 useCallback & useMemo - パフォーマンス最適化

import { useState, useCallback, useMemo } from 'react';

const PowerLevelCalculator = ({ userData, repoData }) => {
  const [multiplier, setMultiplier] = useState(1);
  
  // 重い計算をメモ化
  const powerLevel = useMemo(() => {
    console.log('Calculating power level...');
    
    const basePower = (
      userData.public_repos * 100 +
      userData.followers * 50 +
      userData.following * 10
    );
    
    return basePower * multiplier;
  }, [userData, multiplier]); // 依存配列が変更時のみ再計算
  
  // 関数をメモ化
  const handleScan = useCallback(() => {
    console.log('Scanning user...');
    // スキャン処理
  }, [userData]); // userData が変更時のみ新しい関数を生成
  
  // 配列の処理をメモ化
  const sortedLanguages = useMemo(() => {
    return repoData
      .flatMap(repo => repo.languages)
      .sort((a, b) => b.bytes - a.bytes);
  }, [repoData]);
  
  return (
    <div>
      <h2>Power Level: {powerLevel.toLocaleString()}</h2>
      <button onClick={handleScan}>Rescan</button>
      
      <div>
        <label>
          Multiplier: 
          <input
            type="range"
            min="1"
            max="10"
            value={multiplier}
            onChange={(e) => setMultiplier(Number(e.target.value))}
          />
        </label>
      </div>
      
      <ul>
        {sortedLanguages.map(lang => (
          <li key={lang.name}>{lang.name}</li>
        ))}
      </ul>
    </div>
  );
};

🔧 TypeScript基本文法

🏷️ 型定義

// 基本的な型
let username: string = 'torvalds';
let powerLevel: number = 9001;
let isScanning: boolean = true;

// 配列の型
let languages: string[] = ['JavaScript', 'TypeScript'];
let numbers: Array<number> = [1, 2, 3];

// オブジェクトの型
interface GitHubUser {
  login: string;
  id: number;
  name: string | null; // null許容型
  followers: number;
  following: number;
  public_repos: number;
  created_at: string;
  bio?: string; // オプショナル(undefined許容)
}

// 型エイリアス
type PowerLevel = number;
type UserData = GitHubUser;

// Union型
type Theme = 'light' | 'dark' | 'auto';
type Status = 'loading' | 'success' | 'error';

// 関数の型
type ScanFunction = (username: string) => Promise<GitHubUser>;
type EventHandler = (event: React.MouseEvent<HTMLButtonElement>) => void;

🎨 インターフェース vs 型エイリアス

// インターフェース(推奨:オブジェクト定義)
interface GitHubRepo {
  id: number;
  name: string;
  full_name: string;
  stargazers_count: number;
  language: string | null;
}

// インターフェースの拡張
interface DetailedRepo extends GitHubRepo {
  description: string;
  topics: string[];
  contributors: GitHubUser[];
}

// 型エイリアス(推奨:Union型、計算型)
type ApiResponse<T> = {
  data: T;
  status: 'success' | 'error';
  message?: string;
};

type UserApiResponse = ApiResponse<GitHubUser>;
type RepoApiResponse = ApiResponse<GitHubRepo[]>;

// 条件付き型
type NonNullable<T> = T extends null | undefined ? never : T;
type UserName = NonNullable<GitHubUser['name']>;

🛡️ 型ガード

// ユーザー定義型ガード
function isGitHubUser(data: any): data is GitHubUser {
  return (
    typeof data === 'object' &&
    data !== null &&
    typeof data.login === 'string' &&
    typeof data.id === 'number'
  );
}

// 使用例
const fetchUser = async (username: string): Promise<GitHubUser> => {
  const response = await fetch(`/api/users/${username}`);
  const data = await response.json();
  
  if (isGitHubUser(data)) {
    return data; // 型が確定
  }
  
  throw new Error('Invalid user data');
};

// in演算子を使った型ガード
type LoadingState = { status: 'loading' };
type SuccessState = { status: 'success'; data: GitHubUser };
type ErrorState = { status: 'error'; message: string };

type AppState = LoadingState | SuccessState | ErrorState;

const handleState = (state: AppState) => {
  if ('data' in state) {
    // SuccessState として扱われる
    console.log(state.data.login);
  } else if ('message' in state) {
    // ErrorState として扱われる
    console.error(state.message);
  } else {
    // LoadingState として扱われる
    console.log('Loading...');
  }
};

🎯 実践的なパターン

🔄 カスタムフック

// GitHub API用カスタムフック
const useGitHubApi = () => {
  const [token, setToken] = useState<string | null>(
    localStorage.getItem('github_token')
  );
  
  const getHeaders = useCallback((): HeadersInit => {
    return token ? { Authorization: `token ${token}` } : {};
  }, [token]);
  
  const fetchUser = useCallback(async (username: string): Promise<GitHubUser> => {
    const response = await fetch(`https://api.github.com/users/${username}`, {
      headers: getHeaders(),
    });
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    return response.json();
  }, [getHeaders]);
  
  const fetchRepos = useCallback(async (username: string): Promise<GitHubRepo[]> => {
    const response = await fetch(`https://api.github.com/users/${username}/repos`, {
      headers: getHeaders(),
    });
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    return response.json();
  }, [getHeaders]);
  
  return {
    token,
    setToken,
    fetchUser,
    fetchRepos,
  };
};

// 使用例
const UserProfile = ({ username }: { username: string }) => {
  const { fetchUser } = useGitHubApi();
  const [user, setUser] = useState<GitHubUser | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  
  useEffect(() => {
    let cancelled = false;
    
    const loadUser = async () => {
      setLoading(true);
      setError(null);
      
      try {
        const userData = await fetchUser(username);
        if (!cancelled) {
          setUser(userData);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err instanceof Error ? err.message : 'Unknown error');
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    };
    
    loadUser();
    
    return () => {
      cancelled = true;
    };
  }, [username, fetchUser]);
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>No user data</div>;
  
  return (
    <div>
      <h2>{user.name || user.login}</h2>
      <p>Followers: {user.followers}</p>
      <p>Following: {user.following}</p>
    </div>
  );
};

🎨 コンポーネント合成

// 基本コンポーネント
const Button: React.FC<{
  variant?: 'primary' | 'secondary';
  size?: 'small' | 'medium' | 'large';
  disabled?: boolean;
  onClick?: () => void;
  children: React.ReactNode;
}> = ({ 
  variant = 'primary', 
  size = 'medium', 
  disabled = false, 
  onClick, 
  children 
}) => {
  const className = `button button--${variant} button--${size}`;
  
  return (
    <button
      className={className}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
};

// 合成コンポーネント
const ScanButton: React.FC<{
  isScanning: boolean;
  onScan: () => void;
}> = ({ isScanning, onScan }) => {
  return (
    <Button
      variant="primary"
      size="large"
      disabled={isScanning}
      onClick={onScan}
    >
      {isScanning ? 'SCANNING...' : 'SCAN'}
    </Button>
  );
};

// 高階コンポーネント(HOC)
const withLoading = <P extends object>(
  Component: React.ComponentType<P>
) => {
  return (props: P & { isLoading: boolean }) => {
    const { isLoading, ...rest } = props;
    
    if (isLoading) {
      return <div>Loading...</div>;
    }
    
    return <Component {...(rest as P)} />;
  };
};

// 使用例
const UserProfileWithLoading = withLoading(UserProfile);

🔄 状態管理パターン

// Reducer パターン
type State = {
  user: GitHubUser | null;
  repos: GitHubRepo[];
  loading: boolean;
  error: string | null;
};

type Action =
  | { type: 'FETCH_START' }
  | { type: 'FETCH_USER_SUCCESS'; payload: GitHubUser }
  | { type: 'FETCH_REPOS_SUCCESS'; payload: GitHubRepo[] }
  | { type: 'FETCH_ERROR'; payload: string }
  | { type: 'RESET' };

const initialState: State = {
  user: null,
  repos: [],
  loading: false,
  error: null,
};

const userReducer = (state: State, action: Action): State => {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null };
    
    case 'FETCH_USER_SUCCESS':
      return { ...state, user: action.payload, loading: false };
    
    case 'FETCH_REPOS_SUCCESS':
      return { ...state, repos: action.payload, loading: false };
    
    case 'FETCH_ERROR':
      return { ...state, error: action.payload, loading: false };
    
    case 'RESET':
      return initialState;
    
    default:
      return state;
  }
};

// 使用例
const UserDashboard = () => {
  const [state, dispatch] = useReducer(userReducer, initialState);
  const { fetchUser, fetchRepos } = useGitHubApi();
  
  const handleFetchUser = useCallback(async (username: string) => {
    dispatch({ type: 'FETCH_START' });
    
    try {
      const user = await fetchUser(username);
      dispatch({ type: 'FETCH_USER_SUCCESS', payload: user });
      
      const repos = await fetchRepos(username);
      dispatch({ type: 'FETCH_REPOS_SUCCESS', payload: repos });
    } catch (error) {
      dispatch({ 
        type: 'FETCH_ERROR', 
        payload: error instanceof Error ? error.message : 'Unknown error' 
      });
    }
  }, [fetchUser, fetchRepos]);
  
  return (
    <div>
      <button onClick={() => handleFetchUser('torvalds')}>
        Fetch User
      </button>
      
      {state.loading && <div>Loading...</div>}
      {state.error && <div>Error: {state.error}</div>}
      {state.user && (
        <div>
          <h2>{state.user.name}</h2>
          <p>Repos: {state.repos.length}</p>
        </div>
      )}
    </div>
  );
};

🎯 実践課題

🏆 初級課題

  1. 基本コンポーネント作成

    • ユーザー情報を表示するコンポーネント
    • Props でデータを受け取る
    • 条件分岐でローディング状態を表示
  2. 状態管理

    • useState でフォーム入力を管理
    • useEffect で API 呼び出し
    • エラーハンドリング

🚀 中級課題

  1. カスタムフック作成

    • GitHub API 呼び出し用フック
    • ローディング、エラー状態の管理
    • TypeScript 型定義
  2. パフォーマンス最適化

    • useMemo で重い計算をメモ化
    • useCallback で関数をメモ化
    • React.memo でコンポーネントをメモ化

💪 上級課題

  1. 高階コンポーネント

    • エラーバウンダリー実装
    • ローディング状態のラッパー
    • 認証チェック HOC
  2. 複雑な状態管理

    • useReducer で複雑な状態管理
    • Context API でグローバル状態
    • 型安全なアクション定義

次のステップ: Tips & Best Practicesで実践的な開発テクニックを学びましょう。

Clone this wiki locally