初級11分で読める

【解決】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の初期値が undefinednull のまま子コンポーネントに渡される。これは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で「実際に何が届いているか」を確認するのが解決への最短ルートだ。