はじめに
- 本記事は、UiPath Coded Apps(React+Typescript)の学習用コンテンツです。
- UiPath Coded Apps でアプリを開発するにあたって、UiPathがTypescriptの開発用モジュールやReact+Typescriptで動作するテンプレートを用意してくれているので、基本的にはそれらをもちいて開発をおこないます。
- 筆者自身、HTML/CSS/JavaScriptは扱ったことがあるものの、Reactは初めてだったため、最初理解するのに苦労した部分などを多く書き留めています。
- 記事の内容は、個人の見解または確認結果であり、UiPath の公式見解ではありません。
RPA開発者のための React + TypeScript キャッチアップ
目次
- 0. この記事の前提
- 1. 考え方の転換 UI は state の関数
- 2. コンポーネントと JSX
- 3. 書いてある場所と実行されるタイミングは別物
- 4. props と state
- 5. スナップショット
- 6. 不変更新
- 7. 派生値と state を減らす考え方
- 8. 単方向データフローと state の持ち上げ
- 9. イベント処理
- 10. 制御コンポーネントと非制御コンポーネント
- 11. フックの基本ルール
- 12. useEffect とクリーンアップ
- 13. useRef の2つの顔
- 14. state にするか しないかの判断
- 15. メモ化とカスタムフック
- 16. key の本当の役割
- 17. レンダリングの流れ
- 18. children とコンポジション
- 19. useReducer
- 20. Context は後回しでいい
- 21. コンポーネント設計と作る順番
- 22. TypeScript 型の世界と値の世界
- 23. JSX のお作法集
- 24. 学習の進め方のコツ
- 付録A. 素の HTML JavaScript との対応表
- 付録B. 用語集
0. この記事の前提
- RPA 開発者は「画面を操作する側」の経験は豊富ですが、「画面を作る側」の発想には慣れていません。React はまさにその「作る側」の道具です。
- 素の JavaScript(
document.getElementById(...).innerHTML = ...)の知識があると、かえって React の作法に戸惑います。筆者もそうでした。「素の JS ではこう書くのに、なぜ React ではダメなのか」 を意識すると理解が早くなります。 - TypeScript は「JavaScript に型の注釈を足したもの」です。最初は React の理解を優先し、TypeScript は少しずつ混ぜていくのがおすすめです。
1. 考え方の転換 UI は state の関数
命令的 と 宣言的
- 命令的(素の JS):「ボタンが押されたら、この要素を探して、文字を書き換えて、クラスを付けて…」と 手順 を書く。RPA のワークフローと同じ発想です。
- 宣言的(React):「データがこうなら、画面はこう見える」と 完成形 を書く。書き換えの手順は React がやってくれます。
UI = f(state)
- 画面(UI)は、データ(state)を入れると出てくる関数の結果、という考え方です。
- 開発者がやるのは 「state を変えること」だけ。画面を直接いじることはしません。
- RPA で言えば「セレクターで要素を探して値を書き込む」作業を、もう自分ではやらない、ということです。
2. コンポーネントと JSX
コンポーネントは「画面の部品を返す関数」
function Greeting() {
return <h1>こんにちは</h1>;
}
- 名前は 大文字始まり が決まりです。
-
returnの中に書いたものが画面に出ます。
JSX は HTML ではなく JavaScript
-
<h1>こんにちは</h1>は HTML に見えますが、実体はReact.createElement("h1", null, "こんにちは")という関数呼び出しに変換されます。 - そのため HTML との差分があります:
-
class→className -
for→htmlFor -
onclick="..."→onClick={...}(キャメルケース・文字列ではなく関数を渡す) - 閉じタグ必須(
<br />、<input />) -
returnできるのは 1つのかたまり だけ(複数あるなら<>...</>で囲む)
-
{} の中には「式」だけ
- JSX の中で JavaScript を書くときは
{}で囲みます。 - 入れられるのは 値を返す式 だけ。
if文やfor文は書けません。- 条件分岐 → 三項演算子
a ? b : cやa && b - 繰り返し →
array.map(...)
- 条件分岐 → 三項演算子
⚠️ つまずきポイント 3段に分けて読む
JSX がコンパイル後に Card({ children: ... }) になる、という説明を見て「それも自分で書くの?」と混乱しました。次の3つは別物として読みましょう。
| 段 | 書き方 | 意味 |
|---|---|---|
| 定義する側 | function Card() { ... } |
部品の設計図を作る |
| 使う側 | <Card>...</Card> |
部品を呼び出す(大文字タグ = 自作関数の呼び出し) |
| 画面に出るもの |
Card の return の中身 |
実際に表示される HTML |
コンパイル後の姿は「React の内部ではこうなっている」という裏話であり、自分で書くものではありません。
3. 書いてある場所と実行されるタイミングは別物
筆者が一度完全に詰まったポイントです。 コンポーネント関数の中には、性格の違う行が同居しています。
function App() {
const [count, setCount] = useState(0); // ★ 描画のたびに即実行
function handleClick() { // ☆ 定義されるだけ(まだ動かない)
setCount(count + 1);
}
return <button onClick={handleClick}>{count}</button>; // ★ 即実行
}
- ★ 即実行される行:フック呼び出し、変数の計算、
return - ☆ 定義されるだけの行:
function handleXxx() {}の中身。ボタンが押されるまで動かない
素の HTML + JavaScript で言えば、「<script> に直書きした alert() はページを開いた瞬間に走る/function の中身は onclick などで呼ばれるまで走らない」のと同じです。
onClick={fn} と onClick={fn()} の違い
-
onClick={handleClick}→ 「押されたらこの関数を呼んでね」と 関数そのものを渡す(正しい) -
onClick={handleClick()}→ 描画した瞬間に実行して、その戻り値を渡してしまう(バグ) - 引数を渡したいときは
onClick={() => handleDelete(id)}のように、関数で包みます。
4. props と state
props = 関数の引数
- 親から子へ渡すデータです。JavaScript の関数に引数を渡すのと同じ感覚でOK。
- 実体は オブジェクト1個。
<Card title="A" size={3} />はCard({ title: "A", size: 3 })のイメージです。 - 子は props を 読むだけ。書き換えてはいけません。
type Props = { title: string };
function Card({ title }: Props) {
return <h2>{title}</h2>;
}
state = コンポーネントが覚えておく値
const [count, setCount] = useState(0);
-
count:今の値 -
setCount:値を変える関数(これを呼ぶと再描画が起きる) - 普通の変数
let count = 0ではダメな理由:関数が呼ばれるたびに0に戻ってしまうし、変えても React が気付かないからです。
5. スナップショット
state は「その回の描画時点の値」
- ある描画の中で、
countは ずっと同じ値 です。 -
setCount(count + 1)を呼んでも、その場でcountが変わるわけではありません。次の描画で新しい値になります。
function handleClick() {
setCount(count + 1);
console.log(count); // ← まだ古い値が出る
}
⚠️ つまずきポイント setter の直後に読むと古い
setXxx や dispatch を呼んだ直後に state を読むと古い値です。何度でも引っかかるので、「写真は撮り直さない限り変わらない」と覚えておきましょう。
更新関数形式
setCount(c => c + 1);
setCount(c => c + 1); // これなら +2 になる
- 前の値をもとに計算したいときは、値ではなく 関数 を渡します。
-
setCount(count + 1)を2回書いても、両方とも同じ値のcountを見ているので +1 にしかなりません。
6. 不変更新
配列やオブジェクトは「書き換えずに作り直す」
- React は state が変わったかを
Object.is(参照比較) で判定します。 -
pushなどで中身を直接いじると、「箱(参照)が同じ」なので 変わっていないと判断され、画面が更新されません。
// ❌ NG:同じ配列を書き換えている
memos.push(newMemo);
setMemos(memos);
// ✅ OK:新しい配列を作って渡す
setMemos([...memos, newMemo]);
よく使う非破壊的な書き方
| やりたいこと | 書き方 |
|---|---|
| 追加 | [...list, item] |
| 削除 | list.filter(x => x.id !== id) |
| 1件だけ更新 | list.map(x => x.id === id ? { ...x, done: !x.done } : x) |
| オブジェクトの一部を変更 | { ...obj, name: "新しい名前" } |
-
破壊的メソッド(元を書き換える):
pushpopsplicesortreverse -
非破壊的メソッド(新しいものを返す):
mapfilterconcatslice、スプレッド...
参照比較は重くない
- 「大きなオブジェクトを比較すると重いのでは?」と思いがちですが、
Object.isは 中身を見ずに住所(参照)が同じかだけ を見るので一瞬です。
7. 派生値と state を減らす考え方
- 他の state から計算できる値は、state にせずその場で計算 します。
const [memos, setMemos] = useState<Memo[]>([]);
const doneCount = memos.filter(m => m.done).length; // ← state にしない
- state を増やすと「両方を同期させ忘れる」バグの温床になります。
- ただし派生値にできるのは 何度呼んでも同じ答えを返す純粋な計算 だけです。
URL.createObjectURLのように呼ぶたびに新しいものを「発行」する処理(副作用)は、派生値にできません。
8. 単方向データフローと state の持ち上げ
データは上から下へだけ流れる
- 親 → 子へ props でデータを渡す。子から親へ直接データを戻す手段はありません。
- 子が親の state を変えたいときは、親から「変える関数」を props で受け取って呼ぶ(コールバック props)。
// 親
<MemoItem memo={memo} onDelete={handleDelete} />
// 子
<button onClick={() => onDelete(memo.id)}>削除</button>
state の持ち上げ
- 兄弟コンポーネント同士で同じデータを使いたいときは、state を 共通の親まで引き上げる のが基本です。
- ただしコストもあります:props の数が増える、型が複雑になる(例:
booleanだったものが「どれを編集中か」を表すnumber | nullになる)。 - 逆に、1つの子しか使わない state は 子へ押し下げる 方が見通しが良くなります。
9. イベント処理
合成イベント
- React は画面の各要素にリスナーを付けるのではなく、ルートに1個だけ リスナーを付けて、まとめて振り分けています。
- 受け取るイベントは React が包んだ「合成イベント」です。
TypeScript でのイベントの型
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setText(e.target.value);
}
-
<>の中のHTMLInputElementは「どの要素のイベントか」を示す型引数です。
target と currentTarget
-
target:実際にクリックされた要素(子要素の場合もある) -
currentTarget:リスナーを付けた要素(ハンドラを書いた要素)
10. 制御コンポーネントと非制御コンポーネント
制御コンポーネント
<input value={text} onChange={e => setText(e.target.value)} />
- 入力欄の値を React の state が握る 方式。React の基本です。
-
valueを握るならonChangeを書く義務がセット。書かないと入力できない欄になります。
非制御コンポーネント
- 値は DOM 自身に持たせておき、必要なときに
refで読みに行く方式。
file input は制御できない
-
<input type="file">は セキュリティ上の理由でvalueを代入できません(勝手にファイルを選ばせないため)。 - 例外として
""でリセットすることだけは許されています。
11. フックの基本ルール
-
useで始まる関数(useStateuseEffectuseRefなど)がフックです。 -
ルール1:コンポーネントのトップレベルで呼ぶ(
ifやforの中で呼ばない) - ルール2:React のコンポーネントかカスタムフックの中でだけ呼ぶ
なぜこのルールがあるのか
- React はフックを 名前ではなく「呼ばれた順番」で管理 しています(1番目の useState、2番目の useState…)。
-
ifの中で呼ぶと描画ごとに順番がずれて、別の state と取り違えてしまいます。
12. useEffect とクリーンアップ
useEffect は「React の外の世界とつなぐ」ためのもの
useEffect(() => {
const url = URL.createObjectURL(file);
setPreview(url);
return () => URL.revokeObjectURL(url); // クリーンアップ
}, [file]);
- 第2引数(依存配列)の値が変わったときに実行されます。
[]なら初回だけ。 -
「繋いだら切る」:タイマー、イベント登録、URL の発行など、始めたものは
returnで返す関数(クリーンアップ)で後始末します。
不要な effect を書かない
- 「state から計算できる値を effect で別 state にコピーする」のはアンチパターンです。→ 派生値にしましょう(7章)。
- 「ボタンが押されたら〜する」は effect ではなく イベントハンドラ に書きます。
- effect は最後の手段、くらいに考えるのがちょうどいいです。
⚠️ つまずきポイント [] の effect は初回の値に閉じ込められる
- 依存配列
[]の effect の中から state を読むと、ずっと初回の値 が見えます。 - 最新値を読みたいなら、state ではなく
refを使います(次章)。
13. useRef の2つの顔
顔その1 値の箱
const timerRef = useRef<number | null>(null);
timerRef.current = setTimeout(...);
-
.currentに何でも入れておける箱です。 - ref の性質は「変わらない」ではなく、「変わっても React に知らせない」(再描画が起きない)。
顔その2 DOM への参照
const inputRef = useRef<HTMLInputElement>(null);
<input ref={inputRef} />
inputRef.current?.focus();
- 実際の DOM 要素をつかまえられます。フォーカスや canvas 操作など、React だけではできないことに使います。
- 通常は React → DOM の一方向ですが、ref は DOM → React への逆流 であり、単方向データフローの唯一の例外です。だからこそ使いどころは限定しましょう。
14. state にするか しないかの判断
迷ったら次の順で考えます。
- 他の値から計算できる? → 派生値(state にしない)
- 親から props で届く? → state にしない
- ずっと変わらない? → 普通の定数
-
変わったとき、画面の見た目が変わる?
- 変わる →
useState - 変わらない →
useRef(例:画面外の canvas、タイマー ID、前回の値)
- 変わる →
「静かに間違う」バグに注意
- ref の後始末(
ref.current = null)を忘れても、エラーは出ません。前回の値が出る など、静かに間違った値を返します。React のバグはこのパターンが多いです。
15. メモ化とカスタムフック
useMemo / useCallback / React.memo
- 描画のたびに再計算・再生成しないよう、結果をとっておく仕組みです。
-
「比較する人がいるときだけ使う」 が原則。
React.memoで包んだ子に渡す関数、useEffectの依存配列に入る関数などです。 - 最初は使わなくて大丈夫です。遅くなってから考えましょう。
カスタムフック
-
useXxxという名前で、フックを組み合わせた処理を関数に切り出したもの。 - 「ロジックの再利用」のための仕組みで、state そのものが共有されるわけではありません(呼んだコンポーネントごとに別の state)。
16. key の本当の役割
役割その1 リストの名札
{memos.map(memo => <MemoItem key={memo.id} memo={memo} />)}
-
mapで並べるときはkeyが必須。React が「どれがどれか」を見分けるための名札です。 - 配列のインデックスを key にすると、並び替えや削除で取り違えが起きます。一意で変わらない ID を使いましょう。
役割その2 別物として扱う = state のリセット
-
keyが変わると、React は「別の部品になった」と判断して、中の state を捨てて作り直します。 - 「表示する対象が変わったら入力欄をリセットしたい」といった場面で、effect を書かずに
keyで解決できます。
17. レンダリングの流れ
4つの段階
- Trigger:state が更新される(setter が呼ばれる)
- Render:React がコンポーネント関数を呼び直し、新しい画面の設計図を作る
- Commit:前回との 差分だけ を実際の DOM に反映
- Paint:ブラウザが画面を描く
よくある誤解
-
再レンダリング ≠ ちらつき。 Render は「関数を呼び直すこと」で、実際の DOM は差分しか書き換わりません。
innerHTMLで全部書き換える素の JS とは違います。 - バッチ処理:1つのイベント内で複数の setter を呼んでも、再描画はまとめて1回です。
-
bail out:新しい値が前と同じ(
Object.isで一致)なら、描画をスキップします。 - 再描画は下方向にだけ伝わる:親が再描画されると子も再描画されますが、親や兄弟には伝わりません。
18. children とコンポジション
children = タグの内側に書いた props
function Card({ children }: { children: React.ReactNode }) {
return <div className="card">{children}</div>;
}
<Card>
<p>ここが children になる</p>
</Card>
-
childrenという名前は React が決めた固定の名前 です。 - 型は
React.ReactNode(JSX、文字列、数値など何でも入る)。
コンポジション
- 「材料(データ)を渡して子に組み立てさせる」のではなく、「完成品(JSX)を渡す」 考え方。
- 途中のコンポーネントが使わない props を中継する「バケツリレー」を減らせます。
19. useReducer
state の更新ロジックを1か所にまとめる
type Action =
| { type: "add"; text: string }
| { type: "delete"; id: number };
function reducer(state: Memo[], action: Action): Memo[] {
switch (action.type) {
case "add": return [...state, { id: Date.now(), text: action.text, done: false }];
case "delete": return state.filter(m => m.id !== action.id);
}
}
const [memos, dispatch] = useReducer(reducer, []);
dispatch({ type: "delete", id: 3 });
-
dispatchで「何が起きたか(Action)」を伝え、reducerが「次の state」を計算します。 - 更新の種類が増えてきたら
useStateから乗り換えを検討します。 - Action の型は ユニオン型(
|でつないだ型)で書くのが定番です。 -
dispatchの直後に state を読むと古い値なのはsetXxxと同じ(スナップショット)。
20. Context は後回しでいい
- 深い階層まで props を渡さずにデータを共有する仕組みです。
- 正直、「使わない方が分かりやすい」と感じました。それで正解です。
- 実務でも props → children(コンポジション)→ Context の順で検討するのが定石。必要になってから学べば十分です。
21. コンポーネント設計と作る順番
設計の5ステップ
- 静的な JSX を先に書く(state なし、見た目だけ)
- 変わるものを洗い出す
- state を最小限にする(派生値にできるものは除く)
- state の置き場所を決める(使うコンポーネントの共通の親)
- イベントハンドラを書く(逆方向のデータフロー)
分割の判断基準
- 1つのコンポーネントが複数の役割を持ち始めたら分割を検討。
- リストの1行(
MemoItemなど)は切り出し候補の筆頭。 - 命名慣習:イベントを受け取る props は
onXxx、それを処理する関数はhandleXxx。
⚠️ つまずきポイント 完成品を一度にもらうと壊れる
- 完成したコードを一気に受け取ったら、構造が理解できずに壊してしまいました。
-
Step 0 から1段ずつ積み上げる(毎段必ず動く状態にする)方式に変えたら劇的に理解が進みました。
- 静的JSX →
map/key→useState→ 追加 → 削除 → 完了チェック → 行コンポーネント切り出し → 編集 → state の持ち上げ
- 静的JSX →
- 画面が変わらない「純粋なリファクタリング」の段を挟むのもポイントです。
22. TypeScript 型の世界と値の世界
React + TypeScript のソースで頭がバグる最大の理由がここです。
同じ記号が2つの世界で使い回されている
type Props = { onDelete: (id: number) => void }; // 型の世界
const onDelete = (id: number) => { ... }; // 値の世界
- どちらにも
={}=>が出てきますが、意味が全く違います。 - 上の
onDelete: (id: number) => voidを「number 型の引数」と誤読しましたが、正しくは 「number を受け取って何も返さない関数」という型 です。
型の世界への入口は4つだけ
| 入口 | 例 |
|---|---|
① : の右 |
const n: number = 1 |
② type / interface で始まる行まるごと |
type Memo = { id: number } |
③ <> の中 |
useState<Memo[]>([]) |
④ as の右 |
value as string |
- この入口の中に入ったら、
{}=|=>は 全部型の記号 に化けます。 -
type X = {...}の=は代入ではありません。オブジェクトは作られません。
見分けのコツ
-
:の右は型、=の右は値(ただしtypeの行は丸ごと型)。 -
型の中に
=>があれば、それは関数の型。 -
=>の右が 型名(voidstringなど)なら型、処理 が書いてあれば実コード。 - 関数の型 = 関数から 名前と中身を取り去った残り。
- 型は ビルドすると消える。実行時には存在しません。
import type
import type { Memo } from "./types";
- 「型の世界だけで使うもの」を取り込む書き方。ビルド後には消えます。
React でよく見る型
| 用途 | 型 |
|---|---|
| state の型を明示 |
useState<Memo[]>([])(空配列だと中身の型が推論できないため) |
| children | React.ReactNode |
| input の変更イベント | React.ChangeEvent<HTMLInputElement> |
| 何も返さない関数 | () => void |
| 未選択を表す | number | null |
23. JSX のお作法集
| 書き方 | 意味・注意 |
|---|---|
style={{ color: "red" }} |
外側の {} は「JS を書く」、内側の {} はオブジェクト。二重カッコになる |
disabled={!photo} |
属性に式を渡せる。photo がなければ無効化 |
className={undefined} |
undefined は「この属性を設定しない」扱い |
{a || b || ""} |
左から順に最初の有効な値を使う |
{isOpen && <Modal />} |
条件付き表示。ただし左辺が 0 だと 0 が表示されるので注意 |
<>...</> |
フラグメント。余計な div を増やさずにまとめる |
aria-label="閉じる" |
画面読み上げ向けのラベル。アイコンだけのボタンに付ける |
<dl><dt><dd> |
「項目名:値」のペアを表す HTML 要素 |
visually-hidden クラス |
見た目は隠すが読み上げには残すテクニック |
24. 学習の進め方のコツ
- 1回に1トピック、少量ずつ。 一気に進めると消化不良になります。
- 「なぜ」まで踏み込む。 「フックは呼び出し順で管理される」「変化は参照比較で検知する」など、内部の仕組みを知ると作法に納得できます。
- わざと壊してみる。 「この行を消すとどう壊れるか」を試すと理解が深まります。React のバグは「エラーが出ずに静かに間違う」ことが多いので、なおさら有効です。
- 作る前に読む。 完成した小さなアプリのソースを1本、全体マップ → 1テーマずつ、と読み解く方法も非常に効果的でした。
- 既に知っているものに対応づける。 RPA や素の HTML / JavaScript の知識は無駄になりません(付録A)。
-
小さなアプリを自分で作る。 メモ帳アプリ(追加・削除・完了・編集)は、React の基本がほぼ全部詰まっていておすすめです。Vite で
npm create vite@latestから始められます。
付録A. 素の HTML JavaScript との対応表
| 素の HTML / JavaScript | React |
|---|---|
<body> に書いた HTML |
コンポーネントの return の中の JSX |
<script> 内のイベント用 function
|
イベントハンドラ(handleXxx) |
<script> に直書きした alert()(すぐ走る) |
フック呼び出しや return(描画のたびに走る) |
function の中身(呼ばれるまで走らない) |
function handleXxx() {} の中身 |
| 関数の引数 | props(オブジェクト1個) |
innerHTML の書き換え |
state の更新(画面は React が書き換える) |
getElementById で要素をつかむ |
useRef(ただし最後の手段) |
onclick="Foo()" |
onClick={foo}(文字列ではなく関数を渡す) |
付録B. 用語集
| 用語 | ひとことで |
|---|---|
| コンポーネント | 画面の部品を返す関数(大文字始まり) |
| JSX | JS の中に HTML 風に書ける記法。実体は関数呼び出し |
| props | 親から子へ渡す引数。読み取り専用 |
| state | コンポーネントが覚えておく値。変えると再描画 |
| スナップショット | ある描画時点で固定された state の値 |
| 不変更新 | 元を書き換えず、新しい配列・オブジェクトを作って渡すこと |
| 派生値 | 他の state から計算で求める値。state にしない |
| フック |
use で始まる React の機能関数 |
| 副作用 | 描画以外の外部への影響(通信・タイマー・URL 発行など) |
| クリーンアップ | effect で始めたものを後始末する関数 |
| ref | 変わっても React に知らせない箱/DOM への参照 |
| key | リストの名札。変えると別物扱いで state リセット |
| バッチ処理 | 複数の state 更新をまとめて1回の再描画にすること |
| コンポジション | 完成品の JSX を children などで渡す設計 |
| reducer | 今の state と Action から次の state を計算する関数 |
| ユニオン型 |
A | B のように「どれか」を表す型 |
さいごに
いかがでしたでしょうか。
最初のうちは、出来上がったソースから読み始めると正直わかりにくいです。
1つのAppファイルで、HTMLをハードコードしたモックから少しずつコンポーネントを切り出していくと個人的には理解しやすかったです。
読んでも?が多くつく場合、付録Aから読んでもらうとよいかもしれません。
以上 最後までお読みいただきありがとうございます_(..)