Open3

react&typescriptの勉強

といchといch

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の基本ルール

  1. 単一のルート要素: JSXは必ず単一のルート要素を返す(Fragment <> </> でも可)
  2. クラス名はclassName: HTMLのclassは予約語のため
  3. キャメルケース: イベント名やプロパティ名はキャメルケース(onClick, onChange
  4. Hookは必ずトップレベル: 条件文やループの中では呼び出さない
といchといch

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のベストプラクティス

  1. strict modeを有効化: tsconfig.jsonで"strict": true
  2. anyを避ける: 具体的な型を定義する
  3. 型推論を活用: 明らかな場合は型注釈を省略
  4. インターフェースを優先: オブジェクトの型定義にはinterfaceを使用
  5. 命名規則を統一: PascalCaseでインターフェース/型を定義
といchといch

https://github.com/aToy0m0/react-flow_whywhy-board
このリポジトリを作るときに参考にするために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

初回セットアップ

  1. http://localhost:3000 にアクセスしスーパーアドミンユーザーを作成
  2. テナントを作成
  3. ユーザーを招待(テナントアドミンおよびメンバー)

ユーザー権限

SUPER_ADMIN

  • 全テナントの管理
  • システム設定の変更

TENANT_ADMIN

  • 自テナント内のユーザー管理
  • テナント設定の変更

MEMBER

  • ボードの作成・編集
  • 自分の情報の変更

基本操作

ノード操作

  • 右ハンドルを空白にドラッグしてノード追加
  • 右クリックでメニュー表示(追加・削除)
  • ダブルクリックでテキスト編集

ボード管理

  • 自動保存
  • TOML形式でエクスポート・インポート
  • PNG画像として出力

データベーススキーマ

主要なテーブル:

  • Tenant - 組織情報
  • User - ユーザー情報とロール
  • Board - なぜなぜ分析ボード
  • Node - 分析ノード

詳細は prisma/schema.prisma を参照。

License

  • MIT License