【解決】Reactでpropsがundefinedになる・更新されない原因と対処法
Reactでpropsがundefinedになる・値が変わらない原因を解説。親から正しく渡せていないケース・デフォルト値の設定・オプショナルな型定義・初回レンダリング時の対処まで実例付きで説明します。
propsがundefinedになるとき、「渡したはずなのに」という気持ちになりやすい。でも実際には、いくつかのパターンがあって、それぞれ原因が違う。症状と原因を合わせて確認していこう。
原因1:プロパティ名のタイポ・渡し忘れ(最頻出)
propsは関数の引数と同じで、渡す名前と受け取る名前が完全に一致していないと届かない。
// 親:userName という名前で渡している
<UserCard userName="山田太郎" />
// 子:name という名前で受け取ろうとしている
const UserCard = ({ name }: { name: string }) => (
<p>{name}</p> // undefined になる
)大文字・小文字、スペルが1文字でも違えば別のプロパティとして扱われる。
// ✅ 名前を揃える
const UserCard = ({ userName }: { userName: string }) => (
<p>{userName}</p>
)確認方法: 子コンポーネントで console.log(props) すると、実際に何が渡ってきているかわかる。
const UserCard = (props: { userName: string }) => {
console.log('受け取ったprops:', props) // { userName: '山田太郎' } と表示される
return <p>{props.userName}</p>
}スプレッド構文で渡しているときは特に注意が必要だ。変数名とprops名が対応しているか確認する。
const user = { userName: '山田', age: 30 }
<UserCard {...user} /> // userName と age が展開されて渡される原因2:非同期データの初回レンダリング
APIからデータを取得するまでの間、stateの初期値が undefined や null のまま子コンポーネントに渡される。これはCannotreadProperties系エラーと同じ構造の問題だ。
// ❌ stateの初期値がundefined → 子コンポーネントでエラー
const [user, setUser] = useState() // 初期値がundefined
useEffect(() => {
fetch('/api/user').then(r => r.json()).then(setUser)
}, [])
return <UserCard name={user.name} /> // 最初の描画でuser.nameを参照 → エラー修正は「データがない状態での描画に対応する」こと。
// ✅ 型を明示してnullをガードする
const [user, setUser] = useState<User | null>(null)
return user ? <UserCard name={user.name} /> : <p>ロード中...</p>必要に応じてローディング・エラー状態を分けて管理する。
const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
fetch('/api/user')
.then(r => r.json())
.then(data => {
setUser(data)
setIsLoading(false)
})
.catch(e => {
setError(e.message)
setIsLoading(false)
})
}, [])
if (isLoading) return <p>ロード中...</p>
if (error) return <p>エラー: {error}</p>
if (!user) return <p>ユーザーが見つかりません</p>
return <UserCard name={user.name} />原因3:オブジェクトのデフォルト値不足
propsでオブジェクトを受け取るとき、そのオブジェクト自体が省略可能(?)なのにnullチェックをしていないパターン。
type Props = {
user?: {
name: string
age: number
}
}
// ❌ user が省略されたとき user.name でエラー
const UserCard = ({ user }: Props) => (
<p>{user.name}</p>
)対処方法は3つある。
// ✅ 修正①:オプショナルチェーンで安全に参照
const UserCard = ({ user }: Props) => (
<p>{user?.name ?? '名前なし'}</p>
)
// ✅ 修正②:デフォルト値でundefinedを排除する
const UserCard = ({ user = { name: '名前なし', age: 0 } }: Props) => (
<p>{user.name}</p>
)
// ✅ 修正③:早期リターンで存在保証
const UserCard = ({ user }: Props) => {
if (!user) return <p>ユーザーデータがありません</p>
return <p>{user.name}({user.age}歳)</p>
}TypeScriptの型定義に ? がついていたら、「渡されないことがある」というサインだ。その前提でコンポーネントを書く。
原因4:React.memoで再レンダリングが起きない
React.memo を使っているとき、propsを「毎回新しいオブジェクト」で渡すとmemoが効かない、あるいは逆に毎回再レンダリングされてしまう問題が起きる。
// ❌ { theme: 'dark' } は毎回のレンダリングで新しいオブジェクトが作られる
function Parent() {
return <Child config={{ theme: 'dark' }} />
}
const Child = React.memo(({ config }: { config: { theme: string } }) => {
// config の参照が毎回変わるため memo が効かない
return <div className={config.theme}>コンテンツ</div>
})Reactはpropsの比較に Object.is() を使う。オブジェクトや配列は中身が同じでも、毎回のレンダリングで別の参照として作られるため「変わった」と判定される。
// ✅ useMemoでオブジェクトの参照を安定させる
function Parent() {
const config = useMemo(() => ({ theme: 'dark' }), [])
return <Child config={config} />
}
// ✅ コンポーネントの外で定数として定義(依存しない値ならこれが最もシンプル)
const DEFAULT_CONFIG = { theme: 'dark' } as const
function Parent() {
return <Child config={DEFAULT_CONFIG} />
}
// ✅ プリミティブな値はそのまま渡せる(文字列・数値は参照比較でも正しく動く)
function Parent() {
return <Child theme="dark" />
}原因5:stateを直接変更している
stateのオブジェクトや配列を直接変更してもReactは変化を検知できず、子コンポーネントのpropsが更新されない。
// ❌ stateを直接変更してもReactは気づかない
const [users, setUsers] = useState([{ name: '山田' }])
users[0].name = '鈴木' // NG:参照が変わらない
setUsers(users) // 同じ参照 → Reactは変化を検知しない → 再レンダリングされないReactがstateの変化を検知するには、新しいオブジェクト・配列として渡す必要がある。
// ✅ スプレッド構文で新しいオブジェクトを作る
setUsers(prev => prev.map((u, i) =>
i === 0 ? { ...u, name: '鈴木' } : u
))ネストしたオブジェクトも同様に、変更したい階層を新しいオブジェクトとして作る。
const [form, setForm] = useState({ user: { name: '', age: 0 } })
// ❌ ネストした値を直接変更
form.user.name = '田中' // NG
setForm(form)
// ✅ スプレッド構文で階層ごとに新しいオブジェクトを作る
setForm(prev => ({
...prev,
user: { ...prev.user, name: '田中' }
}))TypeScriptとDevToolsを活用する
TypeScriptの型定義 で必須・省略可能・デフォルト値を明示すると、propsの受け渡しミスをコンパイル時に検知できる。
type UserCardProps = {
name: string // 必須
age?: number // 省略可能
role?: 'admin' | 'user'
onDelete?: () => void
}
const UserCard = ({
name,
age,
role = 'user', // デフォルト値
onDelete,
}: UserCardProps) => (
<div>
<p>{name}</p>
{age !== undefined && <p>{age}歳</p>}
{onDelete && <button onClick={onDelete}>削除</button>}
</div>
)React DevToolsのComponentsタブでは、選択したコンポーネントのpropsをリアルタイムで確認できる。「型は合ってるのに値がおかしい」ときはDevToolsで実際の値を確認するのが最速だ。
このエラーと向き合うための1つの考え方
propsは「親が今この瞬間に渡すことを選んだもの」だけが届く。
名前が一致していなければ届かない。非同期ならまだ届いていない。省略可能なら届いていないこともある。stateが直接変更されたなら、Reactは変化に気づいていない。それぞれ原因が違うので、console.log(props) かDevToolsで「実際に何が届いているか」を確認するのが解決への最短ルートだ。