react&typescriptの勉強
React学習ガイド
React基本概念
コンポーネント
Reactアプリケーションの基本単位。UIの一部を表現する再利用可能なコードブロック。
関数コンポーネント
function MyComponent(props: { title: string }) {
return <h1>{props.title}</h1>;
}
// アロー関数版
const MyComponent = ({ title }: { title: string }) => {
return <h1>{title}</h1>;
};
JSX(JavaScript XML)
JavaScriptの中にHTML風の記法を書ける構文拡張。
const element = <h1>Hello, World!</h1>;
const elementWithExpression = <h1>Hello, {userName}!</h1>;
const elementWithProps = <MyComponent title="React" />;
Props(プロパティ)
親コンポーネントから子コンポーネントに渡すデータ。
// 親コンポーネント
<UserProfile name="太郎" age={25} />
// 子コンポーネント
function UserProfile({ name, age }: { name: string; age: number }) {
return <div>{name}さん({age}歳)</div>;
}
**Ref(useRef)**とは?
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
Refは、DOM要素(HTML要素)に直接アクセスするための仕組み
// textareaRef.current で実際のtextarea要素にアクセス
const el = textareaRef.current;
if (el) {
el.style.height = '100px'; // 直接HTMLを操作
el.focus(); // フォーカスを当てる
}
Hooksとは?
- Reactの機能を「hook(フック)」して使うための関数群
// 代表的なHooks
const [state, setState] = useState(初期値); // 状態管理
const ref = useRef(初期値); // DOM参照
useEffect(() => { }, [依存配列]); // 副作用管理
const result = useCallback(() => { }, [依存配列]); // 関数メモ化
- なぜHooksが必要?
- 関数コンポーネントでも状態管理や副作用処理ができるようになる
- コードの再利用がしやすい
useEffectとは
useEffectは、ReactのHookの一つで、コンポーネントの「副作用」を管理するために使います。
副作用とは?
- コンポーネントの表示以外の処理のことです:
- API呼び出し
- タイマーの設定・解除
- イベントリスナーの追加・削除
- DOM操作
基本的な使い方
useEffect(() => {
// ここに実行したい処理を書く
console.log('コンポーネントが表示された!');
}, []); // ← 依存配列
WhyNode.tsxでの具体例
// 例1: テキストエリアの高さを自動調整(55-64行目)
useEffect(() => {
const el = textareaRef.current;
if (!el) return;
// テキストの量に応じて高さを調整
el.style.height = 'auto';
el.style.height = `${el.scrollHeight}px`;
}, [d.heightHint, d.label]); // labelが変わったら実行
// 例2: コンポーネント削除時のクリーンアップ(118-131行目)
useEffect(() => {
return () => {
// コンポーネントが削除される時に実行される
if (editTimeout) {
clearTimeout(editTimeout); // タイマーを停止
}
if (lockedByMe && d.unlockNode) {
d.unlockNode(id); // ロックを解除
}
};
}, [/* 依存配列 */]);
依存配列の役割
- [] → 1回だけ実行(コンポーネント表示時)
- [value] → valueが変わった時に実行
- なし → 毎回実行(危険)
なぜ必要?
- Reactコンポーネントは何度も再描画されるので、副作用を適切なタイミングで実行・クリーンアップする仕組みが必要だからです。
useState - 状態管理
コンポーネント内で変化する値を管理するHook。
const [count, setCount] = useState(0);
const [text, setText] = useState('');
const [user, setUser] = useState<User | null>(null);
// 使用例
const handleClick = () => {
setCount(count + 1); // 状態更新
};
// 関数型更新(推奨)
const handleClick = () => {
setCount(prev => prev + 1);
};
useCallback - 関数メモ化
関数の再作成を防ぐHook。依存配列の値が変わらない限り、同じ関数インスタンスを返す。
const handleClick = useCallback(() => {
doSomething(id);
}, [id]); // idが変わった時のみ関数を再作成
useMemo - 値メモ化
重い計算結果をキャッシュするHook。
const expensiveValue = useMemo(() => {
return heavyCalculation(data);
}, [data]); // dataが変わった時のみ再計算
イベントハンドリング
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); // デフォルト動作を防ぐ
// フォーム送信処理
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
</form>
);
条件付きレンダリング
// 条件演算子
{isLoggedIn ? <UserDashboard /> : <LoginForm />}
// 論理AND演算子
{error && <ErrorMessage message={error} />}
// 早期リターン
if (loading) return <LoadingSpinner />;
リスト表示
const items = ['apple', 'banana', 'cherry'];
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
// オブジェクト配列の場合
const users = [{ id: 1, name: '太郎' }, { id: 2, name: '花子' }];
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
Reactの基本ルール
-
単一のルート要素: JSXは必ず単一のルート要素を返す(Fragment
<> </>
でも可) -
クラス名は
className
: HTMLのclass
は予約語のため -
キャメルケース: イベント名やプロパティ名はキャメルケース(
onClick
,onChange
) - Hookは必ずトップレベル: 条件文やループの中では呼び出さない
TypeScript学習ガイド
TypeScriptとは?
TypeScriptはJavaScriptに型システムを追加したプログラミング言語。コンパイル時に型チェックを行い、より安全で保守しやすいコードが書ける。
基本的な型
プリミティブ型
// 数値型
let count: number = 42;
let price: number = 99.99;
// 文字列型
let name: string = "太郎";
let message: string = `こんにちは、${name}さん`;
// 真偽値型
let isActive: boolean = true;
let isCompleted: boolean = false;
// null と undefined
let data: string | null = null;
let value: string | undefined = undefined;
配列型
// 配列の書き方(2通り)
let numbers: number[] = [1, 2, 3];
let names: Array<string> = ["太郎", "花子"];
// 多次元配列
let matrix: number[][] = [[1, 2], [3, 4]];
オブジェクト型
// オブジェクトの型定義
let user: {
name: string;
age: number;
email?: string; // オプショナル(?をつける)
} = {
name: "太郎",
age: 25
};
// インデックスシグネチャ
let scores: { [subject: string]: number } = {
math: 90,
english: 85
};
インターフェース(interface)
オブジェクトの形を定義する仕組み。
interface User {
id: number;
name: string;
email: string;
isActive?: boolean; // オプショナル
readonly createdAt: Date; // 読み取り専用
}
const user: User = {
id: 1,
name: "太郎",
email: "taro@example.com",
createdAt: new Date()
};
// インターフェースの継承
interface AdminUser extends User {
role: "admin";
permissions: string[];
}
型エイリアス(type)
既存の型に新しい名前をつける。
type NodeType = "root" | "why" | "cause" | "action"; // Union型
type UserId = string;
type Callback = (data: any) => void;
// オブジェクト型の定義も可能
type Position = {
x: number;
y: number;
};
関数の型
// 関数の型定義
function add(x: number, y: number): number {
return x + y;
}
// アロー関数
const multiply = (x: number, y: number): number => x * y;
// オプショナル引数
function greet(name: string, title?: string): string {
return title ? `${title} ${name}` : name;
}
// デフォルト引数
function createUser(name: string, role: string = "user"): User {
return { id: Date.now(), name, email: "", role };
}
// 関数型の型定義
type EventHandler = (event: Event) => void;
type AsyncFunction = () => Promise<string>;
ジェネリクス(Generics)
型を引数として受け取る仕組み。
// ジェネリック関数
function identity<T>(arg: T): T {
return arg;
}
let result1 = identity<string>("hello"); // string型
let result2 = identity<number>(42); // number型
let result3 = identity("auto"); // 型推論で自動的にstring型
// ジェネリックインターフェース
interface Container<T> {
value: T;
getValue(): T;
}
const stringContainer: Container<string> = {
value: "hello",
getValue() { return this.value; }
};
// 制約付きジェネリクス
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // lengthプロパティがあることを保証
return arg;
}
Union型とIntersection型
// Union型(いずれかの型)
type Status = "loading" | "success" | "error";
let currentStatus: Status = "loading";
type StringOrNumber = string | number;
let value: StringOrNumber = "hello";
value = 42; // OK
// Intersection型(すべての型)
type Timestamped = {
createdAt: Date;
updatedAt: Date;
};
type User = {
name: string;
email: string;
};
type TimestampedUser = User & Timestamped; // 両方の型を含む
型ガード
実行時に型を絞り込む仕組み。
// typeof型ガード
function processValue(value: string | number) {
if (typeof value === "string") {
// この中ではvalueはstring型として扱われる
console.log(value.toUpperCase());
} else {
// この中ではvalueはnumber型として扱われる
console.log(value.toFixed(2));
}
}
// in演算子による型ガード
interface Bird {
fly(): void;
}
interface Fish {
swim(): void;
}
function move(animal: Bird | Fish) {
if ("fly" in animal) {
animal.fly(); // Bird型
} else {
animal.swim(); // Fish型
}
}
// カスタム型ガード
function isString(value: any): value is string {
return typeof value === "string";
}
型アサーション
開発者が型を明示的に指定する仕組み。
// as構文
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
// <型>構文(JSXとの混同を避けるためas構文が推奨)
let strLength2: number = (<string>someValue).length;
// Non-null assertion operator (!)
let element: HTMLElement | null = document.getElementById("myElement");
element!.innerHTML = "Hello"; // nullでないことを保証
便利なユーティリティ型
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Partial<T> - すべてのプロパティをオプショナルに
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; age?: number; }
// Required<T> - すべてのプロパティを必須に
type RequiredUser = Required<PartialUser>;
// Pick<T, K> - 指定したプロパティのみ抽出
type UserSummary = Pick<User, "id" | "name">;
// { id: number; name: string; }
// Omit<T, K> - 指定したプロパティを除外
type UserWithoutId = Omit<User, "id">;
// { name: string; email: string; age: number; }
// Record<K, T> - キーと値の型を指定
type UserRoles = Record<string, "admin" | "user" | "guest">;
// { [x: string]: "admin" | "user" | "guest"; }
モジュールとimport/export
// エクスポート
export interface User {
id: number;
name: string;
}
export const API_URL = "https://api.example.com";
export function createUser(name: string): User {
return { id: Date.now(), name };
}
// デフォルトエクスポート
export default class UserService {
getUser(id: number): Promise<User> {
// 実装
}
}
// インポート
import { User, createUser } from "./user";
import UserService from "./UserService";
import * as UserUtils from "./userUtils";
Reactでよく使う型
import React from "react";
// コンポーネントのProps型
interface ButtonProps {
children: React.ReactNode;
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
disabled?: boolean;
}
const Button: React.FC<ButtonProps> = ({ children, onClick, disabled }) => {
return (
<button onClick={onClick} disabled={disabled}>
{children}
</button>
);
};
// イベントハンドラの型
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// 処理
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
// useStateの型
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState<boolean>(false);
// useRefの型
const inputRef = useRef<HTMLInputElement>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
TypeScriptの設定(tsconfig.json)
{
"compilerOptions": {
"target": "es2020",
"lib": ["dom", "dom.iterable", "es2020"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
],
"exclude": [
"node_modules"
]
}
よくあるエラーと対処法
Property does not exist on type
// エラー例
const user: any = { name: "太郎" };
console.log(user.age); // OK(anyなので)
const user2: { name: string } = { name: "太郎" };
console.log(user2.age); // エラー:Property 'age' does not exist
// 対処法:型を正しく定義するか、オプショナルプロパティを使用
interface User {
name: string;
age?: number;
}
Type assertion vs Type annotation
// Type annotation(推奨)
const count: number = 10;
// Type assertion(必要な場合のみ)
const element = document.getElementById("app") as HTMLDivElement;
TypeScriptのベストプラクティス
-
strict modeを有効化: tsconfig.jsonで
"strict": true
- anyを避ける: 具体的な型を定義する
- 型推論を活用: 明らかな場合は型注釈を省略
- インターフェースを優先: オブジェクトの型定義にはinterfaceを使用
- 命名規則を統一: PascalCaseでインターフェース/型を定義
このリポジトリを作るときに参考にするためにAIにまとめてもらいました
以下、現時点のREADME
WhyWhy Board
なぜなぜ分析(5 Whys)を行うためのWebアプリケーション。組織単位でユーザーを管理し、ボードを共有できる。
機能
- ノードベースのビジュアル分析エディタ
- ドラッグ&ドロップによるノード作成・編集
- Root、Why、Cause、Actionノードによる分析構造化
- PNG形式での図の出力
- 組織(テナント)単位でのユーザー管理
- 3段階のユーザー権限(SUPER_ADMIN、TENANT_ADMIN、MEMBER)
- TOML形式でのデータインポート・エクスポート
技術スタック
- Next.js 14 (App Router)
- React 18
- TypeScript
- PostgreSQL
- Prisma ORM
- NextAuth.js
- React Flow v12
- Tailwind CSS
セットアップ
前提条件
- Docker & Docker Compose
環境構築
cd whywhybord
# 環境変数設定
cp .env.example .env
# NextAuth用のシークレットキー生成
openssl rand -base64 32
# .envファイルを編集(必須項目):
# NEXTAUTH_SECRET=<生成されたキー>
# SUPERADMIN_EMAIL=admin@example.com
# SUPERADMIN_PASSWORD=<任意のパスワード>
# アプリケーションビルド・起動
docker compose up -d --build
# マイグレーション実行
docker compose exec web npx prisma migrate deploy
# Prismaクライアント生成(任意)
docker compose exec web npx prisma generate
# アプリケーション再起動
docker compose restart web
初回セットアップ
- http://localhost:3000 にアクセスしスーパーアドミンユーザーを作成
- テナントを作成
- ユーザーを招待(テナントアドミンおよびメンバー)
ユーザー権限
SUPER_ADMIN
- 全テナントの管理
- システム設定の変更
TENANT_ADMIN
- 自テナント内のユーザー管理
- テナント設定の変更
MEMBER
- ボードの作成・編集
- 自分の情報の変更
基本操作
ノード操作
- 右ハンドルを空白にドラッグしてノード追加
- 右クリックでメニュー表示(追加・削除)
- ダブルクリックでテキスト編集
ボード管理
- 自動保存
- TOML形式でエクスポート・インポート
- PNG画像として出力
データベーススキーマ
主要なテーブル:
-
Tenant
- 組織情報 -
User
- ユーザー情報とロール -
Board
- なぜなぜ分析ボード -
Node
- 分析ノード
詳細は prisma/schema.prisma
を参照。
License
- MIT License