「ReactはJavaScriptのみでTypeScriptを使った書き方をやったことがない」
「Reactフックは何の型名になるのかわからないから簡単な実装もエラーになってしまう」
「学習教材やYoutubeで見かけるReactのコーディングで知らないキーワードがたくさん登場する」
本日はそんな方に向けてReactでTypeScriptを使う方法を解説していきます。
Reactは現代のWeb開発で非常に人気の高いフロントエンドフレームワークですが、その強力な機能と柔軟性をより効果的に活用するためにTypeScriptを導入することは一般的な選択肢の1つとなっています。
本記事ではReactプロジェクトにTypeScriptを統合する方法を初心者向けに解説します。
具体的にはuseState、useReducer、useContext、useRefなどのReactフックを使って、TypeScriptでコンポーネントを記述する手順を紹介します。
これにより安全で保守しやすいコードを書くためのヒントを得ることができるでしょう。
ReactのuseStateでTypeScriptを使う方法
まず基本的な仕組みは変わらないのでシンプルな作りのものについてはuseStateだろうと何だろうと「型推論」が適用されます。
例えば以下のようなtrue、falseがスイッチするだけのものであれば通常のReactの書き方が正しければ型指定は不要です。
import React, { useState } from 'react';
const Login = () => {
const [status, setStatus] = useState(false);
const isLogIn = () => {
setStatus(true);
};
const isLogOut = () => {
setStatus(false);
};
return (
<div>
<button onClick={isLogIn}>ログイン</button>
<button onClick={isLogOut}>ログアウト</button>
<p>ユーザーは{status ? 'ログイン' : 'ログアウト'}しています</p>
</div>
);
};
export default Login;
一方で以下のようなオブジェクト形式になっていて中身のプロパティを自由に作成できるものを初期値にするuseStateであれば型指定が必要になります。
import React, { useState } from 'react';
type User = {
name: string;
email: string;
};
const User = () => {
const [user, setUser] = useState<User>({} as User);
const isLogIn = () => {
setUser({
name: 'yamada',
email: 'test@test.com',
});
};
return (
<div>
<button onClick={isLogIn}>ログイン</button>
<p>ユーザー名:{user.name}</p>
<p>メールアドレス:{user.email}</p>
</div>
);
};
export default User;
useStateと初期値の()の間に<User>のように型定義を指定する書き方です。
また初期値は空のオブジェクトにしていますが、空のオブジェクトにはプロパティと値がありません。
その場合もアサーションを使って型指定をしておきます。
つまり空のオブジェクトだからと言って別の型指定をするわけではないことを教えてあげる必要があるわけです。
一方で上記のコードは以下のようなnullを初期値にした書き方でも同じことになります。
別解として紹介しておきます。
import React, { useState } from 'react';
type User = {
name: string;
email: string;
};
const User = () => {
const [user, setUser] = useState<User | null>(null);
const isLogIn = () => {
setUser({
name: 'yamada',
email: 'test@test.com',
});
};
return (
<div>
<button onClick={isLogIn}>ログイン</button>
<p>ユーザー名:{user?.name}</p>
<p>メールアドレス:{user?.email}</p>
</div>
);
};
export default User;
先ほどは初期値を空のオブジェクトにしていて、今回は初期値を純粋なnullでデータなしとしています。
nullの場合はユニオン型で<User | null>としておけば、データがないときはnullを入れるという意味になります。
1点注意としてはnullの場合はuser.nameやuser.emailのようなデータ出力でエラーになってしまいますので、user?.nameやuser?.emailなどオプショナル型で「データがないときもある」ということを教えてあげることが必要です。
2つの方法のどちらが正解というわけではないので両方知っておくと良いでしょう。
ReactのuseReducerでTypeScriptを使う方法
続いてはuseReducerについてTypeScriptで書く方法です。
useReducerの特徴はstateとtypeとactionでこれら3つに型指定が必要になります。
以下のコードはカウンターを作っています。
import React, { useReducer } from 'react';
type CounterStateType = {
count: number;
};
type CounterActionType = {
type: string;
payload: number;
};
const initialState = { count: 0 };
function reducer(state: CounterStateType, action: CounterActionType) {
switch (action.type) {
case 'increment':
return { count: state.count + action.payload };
case 'decrement':
return { count: state.count - action.payload };
default:
return state;
}
}
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
{/* dispatch関数には型推論が効くので型指定しなくて良い */}
<button onClick={() => dispatch({ type: 'increment', payload: 1 })}>
+
</button>
{state.count}
<button onClick={() => dispatch({ type: 'decrement', payload: 1 })}>
-
</button>
</div>
);
};
export default Counter;
stateはカウンターなのでnumberにしていて、typeはアクション名なのでstring、actionはstateを更新するものなのでstateと同じくnumberにしています。
ちなみにactionを呼び出すdispatch関数については型推論が働くため明示的に型指定を書かなくて大丈夫になっています。
これでも問題ないのですがtypeについてはstringとすることで、間違ったtype名も許してしまうことになるので実務では以下のようにタイプ名をそのままユニオン型で指定することが多いです。
import React, { useReducer } from 'react';
type CounterStateType = {
count: number;
};
type CounterActionType = {
type: 'increment' | 'decrement';
payload: number;
};
const initialState = { count: 0 };
function reducer(state: CounterStateType, action: CounterActionType) {
switch (action.type) {
case 'increment':
return { count: state.count + action.payload };
case 'decrement':
return { count: state.count - action.payload };
default:
return state;
}
}
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<button onClick={() => dispatch({ type: 'increment', payload: 1 })}>
+
</button>
{state.count}
<button onClick={() => dispatch({ type: 'decrement', payload: 1 })}>
-
</button>
</div>
);
};
export default Counter;
他にもタイプの種類が増えた時のことも考えてみます。
カウンターでプラスとマイナスだけですがリセットも追加してみます。
import React, { useReducer } from 'react';
type CounterStateType = {
count: number;
};
type CounterActionType = {
// ここを変更
type: 'increment' | 'decrement' | 'reset';
payload: number;
};
const initialState = { count: 0 };
function reducer(state: CounterStateType, action: CounterActionType) {
switch (action.type) {
case 'increment':
return { count: state.count + action.payload };
case 'decrement':
return { count: state.count - action.payload };
// ここを追加
case 'reset':
return initialState;
default:
return state;
}
}
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<button onClick={() => dispatch({ type: 'increment', payload: 1 })}>
+
</button>
{state.count}
<button onClick={() => dispatch({ type: 'decrement', payload: 1 })}>
-
</button>
{/* ここを追加 */}
<button onClick={() => dispatch({ type: 'reset', payload: 0 })}>
リセット
</button>
</div>
);
};
export default Counter;
まず型指定のCounterActionTypeについてユニオン型にresetというタイプ名を追加しました。
続いて関数reducerでresetというタイプの処理を書くわけですが、0に戻すだけですのでinitialStateを出しました。
最後に画面の表示部分でボタンを作成してdispatch関数でresetを呼び出しせばOKです。
注意点としてはresetはただ0にするだけなのでpayloadは不要ですが、型指定のCounterActionTypeでpayloadを宣言しているため不要でも書かないといけません。
そのためpayloadにも0を入れておきました。
これが少し違和感がある場合には、タイプごとに型指定を分ける書き方があります。
import React, { useReducer } from 'react';
// ここを変更
type ChangeCountType = {
type: 'increment' | 'decrement';
payload: number;
};
type ResetCountType = {
type: "reset";
}
type CounterStateType = ChangeCountType | ResetCountType;
const initialState = { count: 0 };
function reducer(state: CounterStateType, action: CounterActionType) {
switch (action.type) {
case 'increment':
return { count: state.count + action.payload };
case 'decrement':
return { count: state.count - action.payload };
case 'reset':
return initialState;
default:
return state;
}
}
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<button onClick={() => dispatch({ type: 'increment', payload: 1 })}>
+
</button>
{state.count}
<button onClick={() => dispatch({ type: 'decrement', payload: 1 })}>
-
</button>
{/* ここを変更 */}
<button onClick={() => dispatch({ type: 'reset'})}>
リセット
</button>
</div>
);
};
export default Counter;
ChangeCountTypeという型指定ではincrementとdecrementの2種類のタイプだけを許す型指定にしています。
それとは別にResetCountTypeという型指定でresetのみのタイプだけを許す型指定を作成しています。
最後に元あったCounterAcrionTypeではChangeCountTypeとResetActionTypeの2つの型指定をユニオン型で受け入れて、結果的にincrement | decrement | resetと同じような意味合いにしています。
これによって画面表示の部分でリセットボタンのdispatch関数にはtypeのみの指定で不要なpayloadは書かなくてもコンパイルが通ることになりました。
ReactのuseContextでTypeScriptを使う方法
続いてはuseContextになります。
useContextもTypeScriptでは型推論が効きますので自分で書くことは無くなりました。
型名としてはReact.Contextと言う型名がuseContextには必要でした。
以下のコードはCSSスタイルをtheme.tsと言うファイルで宣言しておき、theme.tsをグローバルステートとしてコンポーネントファイルに使用するような内容です。
export const theme = {
firstTheme: {
main: '#121481',
sub: '#fff',
},
secondTheme: {
main: '#8DECB4',
sub: '#fff',
},
};
import React, { createContext } from 'react';
import { theme } from './theme';
type ThemeContextProviderProps = {
children: React.ReactNode;
};
export const ThemeContext = createContext(theme);
const ThemeContextProvider = ({ children }: ThemeContextProviderProps) => {
return (
<ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
);
};
export default ThemeContextProvider;
import React, { useContext } from 'react';
import { ThemeContext } from './ThemeContextProvider';
const Card = () => {
const theme = useContext(ThemeContext);
return (
<div
style={{
backgroundColor: theme.firstTheme.main,
color: theme.firstTheme.sub,
}}
>
<h1>Card</h1>
</div>
);
};
export default Card;
childrenを使用しているためchildren自体にはReact.ReactNodeと言う型名を指定しましたが、useContextとcreateContextには型推論が効いているので型指定を行っておりません。
しかしuseContextの中でuseStateを使うときには前章でやった内容のように型指定が必要になります。
別の例題を示してみます。
ユーザーがログインしていれば名前とメールアドレスを画面に表示するコードを作っています。
import React, { useContext } from 'react';
import { UserContext } from './UserContextProvider';
const User = () => {
const isLogin = () => {
userContext?.setUser({
name: 'yamada',
email: 'yamada@sample.com',
});
};
const isLogout = () => {
userContext?.setUser(null);
};
const userContext = useContext(UserContext);
return (
<div>
<button onClick={isLogin}>ログイン</button>
<button onClick={isLogout}>ログアウト</button>
<h1>ユーザー名:{userContext?.user?.name}</h1>
<h1>メールアドレス:{userContext?.user?.email}</h1>
</div>
);
};
export default User;
import { createContext, useState } from 'react';
export type User = {
name: string;
email: string;
};
type UserContextType = {
user: User | null;
setUser: React.Dispatch<React.SetStateAction<User | null>>;
};
type UserContextProviderProps = {
children: React.ReactNode;
};
export const UserContext = createContext<UserContextType | null>(null);
const UserContextProvider = ({ children }: UserContextProviderProps) => {
const [user, setUser] = useState<User | null>(null);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
export default UserContextProvider;
childrenについては引き続きReact.ReactNodeと言う型名を指定しています。
またuseStateのステート変数userについてはユーザー情報の名前とメールアドレスについての型指定をstringとしています。
さらにuseStateのステート関数setUserについてはReact.Dispatch<React.SetStateActionと言う型名を指定しています。
こちらはステート関数専用の型名になりますがマウスホバーして確認することができるので覚える必要はありません。
ReactのuseRefでTypeScriptを使う方法
続いてはuseRefについてです。
useRefはinputタグのようなHTMLタグの属性のような使い方ができるフックです。
まず最初に特定のinputタグにマウスのフォーカスを当てる内容を作ってみます。
import React, { useEffect, useRef } from 'react';
const DomRef = () => {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return (
<div>
<input type="text" />
<input type="text" ref={inputRef} />
</div>
);
};
export default DomRef;
ref属性にuseRefをつけているinputタグにマウスのフォーカスが当たっているのがわかります。
useRefの初期値をnullにしておきuseEffectの初回リロードでfocusメソッドを実行したためです。
useRefにはref属性を指定したタグの型名を指定することになっていて、今回だとinputタグなのでHTMLInputElementになりました。
また型指定ではないですが初期値をnullにしているときはnullチェックでinputRef.current?.focus()のようにfucusメソッドにオプショナルチェーンを指定します。
オプショナルチェーンを使わない方法で書く方法もあって以下のように書きます。
import React, { useEffect, useRef } from 'react';
const DomRef = () => {
// ここを修正
const inputRef = useRef<HTMLInputElement>(null!);
useEffect(() => {
// ここを修正
inputRef.current.focus();
}, []);
return (
<div>
<input type="text" />
<input type="text" ref={inputRef} />
</div>
);
};
export default DomRef;
続いて別の例題をやってみます。
以下のコードはタイマーが走っている中で「ストップ」ボタンをクリックすると、クリックした時点でタイマー表示が固定されるものです。
import React, { useEffect, useRef, useState } from 'react';
const MutableRed = () => {
const [timer, setTimer] = useState(0);
const intervalRef = useRef<number | null>(null);
const stopTimer = () => {
if (intervalRef.current) window.clearInterval(intervalRef.current);
};
useEffect(() => {
intervalRef.current = window.setInterval(() => {
setTimer((timer) => timer + 1);
});
return () => {
stopTimer();
};
}, []);
return (
<div>
{timer}
<button onClick={() => stopTimer()}>ストップ</button>
</div>
);
};
export default MutableRed;
上記コードの例だとuseRefに対してnumber型もしくはnull型を指定しています。
先ほどはref属性のあるHTML属性の型名でしたが、今回はHTMLタグのref属性ではなくwindowオブジェクトのDOMからタイマーの値を取得するのでシンプルなnumber型を使うことになっています。
このようにHTMLタグのref属性で使うとき以外ではDOMから参照する値の型名をシンプルに指定すれば良いわけです。