起こったこと
項目を新規追加するためのフォームを作成していました。
こういうコンポーネントがあって
import { useState } from 'react'
import blogService from '../services/blogs'
import PropTypes from 'prop-types'
const BlogForm = ({blogs, setBlogs}) => {
const [blogFormItems, setblogFormItems] = useState({})
const handleFormOnChange = (name, value) => {
const inputs = {...blogFormItems}
switch (name) {
case 'title':
inputs.title = value
setblogFormItems(inputs)
break
case 'author':
inputs.author = value
setblogFormItems(inputs)
break
case 'url':
inputs.url = value
setblogFormItems(inputs)
break
default:
setblogFormItems(inputs)
}
}
const submit = async (event) => {
event.preventDefault()
const newObj = {...blogFormItems}
const createdBlog = await blogService.create(newObj)
setBlogs(blogs.concat(createdBlog))
}
return (
<div>
<h1>create new</h1>
<form method="post" onSubmit={submit}>
title: <input type="text" value={blogFormItems.title} onChange={(e) => handleFormOnChange('title', e.target.value)} /><br />
author: <input type="text" value={blogFormItems.author} onChange={(e) => handleFormOnChange('author', e.target.value)} /><br />
url: <input type="text" value={blogFormItems.url} onChange={(e) => handleFormOnChange('url', e.target.value)} /><br />
<button type="submit">create</button>
</form>
</div>
)
}
BlogForm.propTypes = {
blogs: PropTypes.array.isRequired,
setBlogs: PropTypes.func.isRequired
}
export default BlogForm
こういうふうに使っていました
//(略)
const App = () => {
//(略)
return (
<div>
{/*略*/}
{user && <BlogForm blogs={blogs} setBlogs={setBlogs}/>}
{user && <BlogList blogs={blogs} />}
</div>
)
}
export default App;
実際にフォームに値を入力してsubmitすると、以下のようなエラーが発生しました。
Warning: A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component.
エラーメッセージを読んでみる
読んでみると、コンポーネントがuncontrolledからcontrolledに変更されたことによるエラーだということがわかります。エラーメッセージによると、値がundefinedからundefinedじゃない状態に変わる時に起こりうるとのこと。
controlledとuncontrolledというのがいまいち理解できてなかったので、確認しました。
要はこういうことのようです。
//uncontrolled
<input type="text" />
//controlled
<input value={someValue} onChange={handleChange} />
理解はしましたが、そもそもuncontrolledになる意味がよくわかりません。
フォーム自体は以下のようになっており、完全にcontrolledなフォームです。
<form method="post" onSubmit={submit}>
title: <input type="text" value={blogFormItems.title} onChange={(e) => handleFormOnChange('title', e.target.value)} /><br />
author: <input type="text" value={blogFormItems.author} onChange={(e) => handleFormOnChange('author', e.target.value)} /><br />
url: <input type="text" value={blogFormItems.url} onChange={(e) => handleFormOnChange('url', e.target.value)} /><br />
<button type="submit">create</button>
</form>
加えて、値がundefinedからdefinedになるような箇所にも心当たりがありません。
各inputのvalueに設定しているblogFormItemsには空のオブジェクトが初期値として設定されていますので、undefinedではないはずです。
const [blogFormItems, setblogFormItems] = useState({})
と、ここまで考えたところで
空オブジェクト => フィールドが何も存在しない => valueの初期値は "空オブジェクト.存在しないフィールド"、つまりundefinedなのでは...?
という部分に考えが至りました。
存在しないフィールドなのでundefinedになっていた
↑ググってヒットしたstackoverflow
やっぱり
空オブジェクト => フィールドが何も存在しない => valueの初期値は "空オブジェクト.存在しないフィールド"、つまりundefined
という解釈で合っていたようです。そして調べたところ、controlledなフォームでも初期値がnullかundefinedの場合はuncontrolledとみなされるとのこと。
上記のstackoverflowでは
・短絡評価を使う
・初期値のオブジェクトにフィールドを設定する
という2通りの解決策が提示されています。
修正
valueの値に短絡評価を使う場合
<form method="post" onSubmit={submit}>
title: <input type="text" value={blogFormItems.title || ''} onChange={(e) => handleFormOnChange('title', e.target.value)} /><br />
author: <input type="text" value={blogFormItems.author || ''} onChange={(e) => handleFormOnChange('author', e.target.value)} /><br />
url: <input type="text" value={blogFormItems.url || ''} onChange={(e) => handleFormOnChange('url', e.target.value)} /><br />
<button type="submit">create</button>
</form>
初期値のオブジェクトにフィールドを設定する場合
//必要なフィールドを持たせる
const [blogFormItems, setblogFormItems] = useState({
title: '',
author: '',
url: ''
})
//(略)
const submit = async (event) => {
event.preventDefault()
const newObj = {...blogFormItems}
const createdBlog = await blogService.create(newObj)
setBlogs(blogs.concat(createdBlog))
//初期化する時も忘れずフィールドを持たせる
setblogFormItems({
title: '',
author: '',
url: ''
})
}
//フォームはそのまま
return (
<div>
<h1>create new</h1>
<form method="post" onSubmit={submit}>
title: <input type="text" value={blogFormItems.title} onChange={(e) => handleFormOnChange('title', e.target.value)} /><br />
author: <input type="text" value={blogFormItems.author} onChange={(e) => handleFormOnChange('author', e.target.value)} /><br />
url: <input type="text" value={blogFormItems.url} onChange={(e) => handleFormOnChange('url', e.target.value)} /><br />
<button type="submit">create</button>
</form>
</div>
)
//(略)
初期化する際に誤って空のオブジェクトを引数に設定してしまう恐れがあるので、今回の場合はvalueに短絡評価を使用する方法で修正しました。
inputの項目数が少ないので、各項目useStateで初期値に空文字を設定して管理するという方法もありなのかなと思いました。
というか、最初からそうしていれば今回のエラーは起こらなかったのでは...