React useRefでcurrentが読み取り専用になる問題の解決策
問題の説明
ReactでuseRef
フックを使用する際、次のようなTypeScriptエラーが発生することがあります:
typescript
Cannot assign to 'current' because it is a read-only property
このエラーは主に、useRef
の型定義が不適切な場合に発生します。特に以下のようなコードで顕著です:
tsx
const unblockRef = useRef<() => void | null>(null);
// 後に実行するとエラーが発生
unblockRef.current = null;
エラーが発生する原因
- 型定義の解釈ミス:
() => void | null
という型は「voidまたはnullを返す関数」と解釈されます - 読み取り専用プロパティ: この型定義だと、TypeScriptは
current
を変更不可と判断 - 関数型の優先順位: TypeScriptの型演算子
|
の優先順位が原因で意図しない解釈が発生
なぜ「読み取り専用」になるのか?
ReactのuseRef
には2種類の型が存在します:
RefObject
:current
プロパティが読み取り専用(主にDOM参照用)MutableRefObject
:current
プロパティが変更可能(可変値保持用)
解決方法
方法1: 型定義を正確に括弧で囲む(推奨)
tsx
// 修正前(エラー発生)
const unblockRef = useRef<() => void | null>(null);
// 修正後(動作する)
const unblockRef = useRef<(() => void) | null>(null);
変更点の解説:
- 関数型
() => void
を括弧()
で明確に囲む | null
で「関数またはnull」という意図を正確に表現- TypeScriptが
MutableRefObject
を正しく推論するようになる
tsx
// エラーが発生するパターン
const unblockRef = useRef<() => void | null>(null);
useEffect(() => {
unblockRef.current = null; // Error!
}, []);
tsx
// 正しい型定義で動作
const unblockRef = useRef<(() => void) | null>(null);
useEffect(() => {
unblockRef.current = null; // OK
unblockRef.current = () => console.log("Fixed!"); // OK
}, []);
方法2: MutableRefObjectを明示的に指定する
型推論に依存せず、直接MutableRefObject
型を宣言する方法もあります:
tsx
import { MutableRefObject, useRef } from 'react';
const unblockRef: MutableRefObject<(() => void) | null> = useRef(null);
どちらの方法を選ぶべきか?
- 一般的には方法1が簡潔で推奨されます
- 複雑な型の場合は方法2で明示的に型指定すると可読性が向上
技術的な背景
TypeScriptの型解釈の仕組み
下記の型定義は、TypeScriptによって異なる方法で解釈されます:
ts
() => void | null // 解釈: () => (void | null)【間違い】
(() => void) | null // 解釈: (関数型) または null【正しい】
Reactのrefオブジェクトの種類
型 | current の性質 | 主な用途 |
---|---|---|
RefObject | 読み取り専用 | DOM要素の参照 |
MutableRefObject | 変更可能 | コンポーネント内の可変値保持 |
型推論の自動決定ルール
useRef
の初期値と型パラメータからReactが自動決定:
null
許容型 →MutableRefObject
と推論- 非許容型 →
RefObject
と推論
よくある関連エラーと対処法
関数以外の型での同様のエラー
文字列など他の型でも発生する可能性があります:
tsx
// 問題のある例
const countRef = useRef<number>(0);
// 修正方法
const countRef = useRef<number | null>(null);
DOM参照での注意点
DOM要素を参照する場合はRefObject
(読み取り専用)が適切です:
tsx
// DOM参照では変更不可であるべき
const divRef = useRef<HTMLDivElement>(null);
// 以下は実行すべきでない(Reactが管理するDOMを直接操作)
divRef.current = anotherElement; // 非推奨
複合型の正しい定義例
tsx
// 複数の型を使う場合の適切な定義
const multiRef = useRef<string | (() => void) | null>(null);
// 更新例
multiRef.current = "text"; // OK
multiRef.current = () => alert("Hello"); // OK
multiRef.current = null; // OK
まとめ
useRef
でcurrent
プロパティへの代入エラーが発生する主な原因と解決策は:
- 型定義が不正確 → 関数型は
( )
で明確に囲むtsuseRef<(() => void) | null>(null)
- 読み取り専用プロパティ問題 →
null
許容型でMutableRefObject
を推論させる
TypeScriptの型演算子の優先順位とReactのrefの特性を理解することで、このようなエラーを効果的に回避できます。