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