TypeScriptでuseRefとuseContextの型定義をマスター!React開発の初心者向け完全ガイド
生徒
「Reactで便利なuseRefやuseContextを使いたいのですが、TypeScriptだと型のエラーが出てしまって難しいです。」
先生
「そうですね。TypeScriptでは、そのデータが何なのかを事前に教えてあげる『型定義』がとても大切になります。」
生徒
「初心者でも失敗しない、正しい型の書き方を教えてもらえますか?」
先生
「もちろんです!今回は、初心者の方が特につまずきやすいポイントを丁寧に解説していきますね。」
1. TypeScriptとReactのフックについて学ぼう
プログラミングを始めたばかりの方にとって、「型」という言葉は少し難しく感じるかもしれません。型とは、簡単に言うと「その箱(変数)の中に何を入れるかのルール」のことです。TypeScriptを使う最大のメリットは、このルールをあらかじめ決めておくことで、間違ったものを入れようとしたときにパソコンが「それは違いますよ!」と教えてくれる点にあります。
Reactという道具を使って画面を作る際によく使われるのが「フック」と呼ばれる機能です。その中でも、画面上の部品を直接操作するためのuseRefや、複数の部品でデータを共有するためのuseContextは非常に強力です。これらをTypeScriptで使うとき、どのようなルール(型)を適用すればよいのか、基本からじっくり学んでいきましょう。
2. useRefの基本的な役割と型定義の重要性
useRefは、主に二つの役割を持っています。一つは、入力フォームに自動でカーソルを合わせるなど、HTMLの要素を直接触りたいとき。もう一つは、画面を書き換えずに値を保存しておきたいときです。パソコンに詳しくない方向けに例えると、useRefは「特定の場所に貼り付けた付箋」や「指差し確認のための目印」のようなものです。
TypeScriptでは、この付箋が「どの種類の部品を指しているのか」を正確に指定する必要があります。例えば、文字を入力する場所なのか、ボタンなのか、それともただの数字を覚えているだけなのかを区別します。これを正しく行わないと、プログラムは「そこにあるのが何かわからないので、操作させられません」と機嫌を損ねてしまいます。まずは、一番よく使う「HTML要素を操作する場合」の書き方を見てみましょう。
3. useRefでHTML要素に型を付ける方法
例えば、ボタンをクリックしたときに入力欄に色を付けたり、文字を自動で入力したりしたい場合があります。このとき、useRefの中身が「HTMLの入力欄(input要素)」であることを教える必要があります。TypeScriptには、あらかじめHTMLInputElementやHTMLButtonElementといった名前の型が用意されています。
以下のコードは、入力フォーム(input)を操作するための基本的な型定義の例です。初心者のうちは、nullで初期化することをセットで覚えておくと、エラーを防ぎやすくなります。これは、画面が表示される瞬間までは、まだその部品が準備できていない可能性があるためです。
import React, { useRef } from 'react';
const TextInputFocus = () => {
// HTMLのinput要素を指すための型を指定します
// 初期値は「まだ何もない」という意味のnullを入れます
const inputRef = useRef<HTMLInputElement>(null);
const handleClick = () => {
// currentの中身があるか確認してから操作します
if (inputRef.current) {
inputRef.current.focus();
console.log("入力欄にカーソルを合わせました");
}
};
return (
<input ref={inputRef} type="text" />
<button onClick={handleClick}>フォーカスを当てる</button>
);
};
このように、<HTMLInputElement>のように不等号で囲って型を書く方法を「ジェネリクス」と呼びます。これは「この道具(useRef)を、HTMLInputElement専用として使います」という宣言になります。
4. 値の保持にuseRefを使う場合の型定義
useRefは、画面の見た目に関係のないデータをこっそり持っておくのにも使えます。例えば、タイマーのIDや、前の画面の状態を覚えておくときなどです。この場合は、操作したいデータの種類(数値ならnumber、文字ならstring)を指定します。HTML要素ではないので、初期値に具体的な数字を入れることが多いです。
import React, { useRef } from 'react';
const CounterRef = () => {
// 数字(number型)を保存するためのuseRefです
// 初期値として「0」を入れます
const countRef = useRef<number>(0);
const increment = () => {
countRef.current += 1;
console.log("現在の値は: " + countRef.current);
};
return (
<p>コンソールで値を確認してください</p>
<button onClick={increment}>カウントアップ</button>
);
};
実行結果は以下のようになります。ボタンを押しても画面の数字は変わりませんが、プログラムの内部ではしっかりと数字が増えていきます。
現在の値は: 1
現在の値は: 2
現在の値は: 3
5. useContextとは?データのバケツリレーを防ぐ魔法
次にuseContextについて解説します。通常、Reactでは親から子、子から孫へとデータを手渡ししていきます。これを「プロップス(Props)」と呼びますが、あまりに階層が深いと、途中の部品が自分では使わないデータをただ渡すだけになってしまいます。これを「バケツリレー」と呼び、プログラムが複雑になる原因となります。
useContextは、このバケツリレーを飛ばして、必要なデータをどこからでも直接取り出せるようにする仕組みです。いわば、家の中のどこにいてもアクセスできる「共有の冷蔵庫」のようなものです。しかし、冷蔵庫に何が入っているか分からないと困るように、TypeScriptでも「この共有データには何が入っているのか」を明確に定義しなければなりません。
6. useContextの型定義:Contextの作成と提供
useContextを使うには、まず「どんなデータを共有するか」という設計図を作ることから始めます。これを「コンテキストの作成」と言います。初心者が特につまずくのは、初期値の設定です。最初はデータが空っぽであることが多いため、型定義には「データの形、または未定義(undefined)」といった柔軟性を持たせることが一般的です。
以下の例では、ユーザーの名前をアプリ全体で共有するための型定義を行っています。interfaceという言葉を使って、データの構造に名前を付けています。これは「このデータはこういう形をしていますよ」という証明書のようなものです。
import React, { createContext, useContext } from 'react';
// 1. 共有したいデータの形を定義します
interface UserConfig {
name: string;
isLoggedIn: boolean;
}
// 2. コンテキストを作成します。最初はデータがないかもしれないので
// UserConfig型か、もしくはundefinedが許容されるようにします
const UserContext = createContext<UserConfig | undefined>(undefined);
const UserDisplay = () => {
// 3. データを呼び出します
const user = useContext(UserContext);
// データがなかった場合の処理を必ず書きます(TypeScriptのルール)
if (!user) {
return <p>ログインしていません</p>;
}
return (
<div>
<p>こんにちは、{user.name}さん!</p>
</div>
);
};
7. useContextで関数を共有する場合のテクニック
データだけでなく、データを書き換えるための「関数(機能)」を共有することもよくあります。例えば、ダークモードへの切り替えスイッチなどです。関数を型定義するときは、引数(入れるもの)と戻り値(返ってくるもの)を指定します。何も返さない関数の場合は、void(ボイド)という特殊な言葉を使います。これは「空っぽ」という意味です。
import React, { createContext, useState, ReactNode } from 'react';
// テーマの種類と、それを変えるための関数の形を決めます
interface ThemeContextType {
theme: string;
toggleTheme: () => void;
}
// コンテキストを作成
export const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// プロバイダーという「データを提供する親」を作ります
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
const [theme, setTheme] = useState("light");
const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
ここで出てきたReactNodeは、Reactが画面に表示できるあらゆるもの(文字、タグ、他の部品など)を指す非常に便利な型です。これを使うことで、どんな部品でも包み込むことができるようになります。
8. エラーを防ぐ!カスタムフックでの安全な取り出し方
useContextを直接使うと、先ほどのコードのように毎回「データが空(undefined)ではないか」を確認しなければなりません。これは少し面倒ですし、忘れるとエラーの原因になります。そこで、開発現場では「カスタムフック」という自分専用の道具を作って、安全にデータを取り出す工夫をします。
以下のコードは、データが見つからない場合に分かりやすいエラーメッセージを出してくれる、とても親切な仕組みです。これを用意しておくだけで、開発の効率がぐんと上がります。
import { useContext } from 'react';
import { ThemeContext } from './ThemeProvider';
// 独自の「useTheme」というフックを作ります
export const useTheme = () => {
const context = useContext(ThemeContext);
// もしThemeProviderの外で使おうとしたらエラーを出します
if (context === undefined) {
throw new Error("useThemeはThemeProviderの中で使ってください");
}
return context;
};
このように、「もしもの時」の処理をあらかじめ書いておくのが、良いプログラムを作るコツです。パソコンに慣れていない方でも、このパターンを覚えておけば、複雑なアプリ開発でも迷わずに進めるようになります。TypeScriptは厳しい先生のように思えるかもしれませんが、実は私たちがミスをしないようにずっと見守ってくれている優しいガイドなのです。