タイトル通りです。
JSのArrayは参照渡しということがわかっていなかったのと、ReactのuseStateの理解が浅かったことが原因で若干時間をとってしまいました。
起こったこと
(コンポーネントの設計が色々とアレなのは許してください...)
一覧表示画面を実装していました。
//色々略
const App = () => {
//略
const [blogs, setBlogs] = useState([])
//略
return (
<div>
{/*略*/}
{user && <BlogList blogs={blogs} setBlogs={setBlogs} />}
</div>
)
}
export default App;
↑のBlogListというのが一覧です。中身はこうなっています
const BlogList = ({ blogs, setBlogs}) => {
return (
<ul className="blog_ul">
{blogs.map(blog =>
<Blog key={blog.id} blog={blog} blogList={blogs} setBlogs={setBlogs} />
)}
</ul>
)
}
Appコンポーネントからblogsステートとblogsを更新するためのsetBlogsをpropsとして受け取り、さらにリスト要素のBlogコンポーネントに渡しています。こういうバケツリレーをやっていいのかやるべきでないのか正直自信ないですが、とりあえずこうなってます。
Blogコンポーネントの中身は以下の通りです。
const Blog = ({blog, blogList, setBlogs}) => {
const [showDetail, setShowDetail] = useState(false)
const toggleDetail = () => {
setShowDetail(!showDetail)
}
const incrementLike = async blog => {
const currentBlogList = blogList
const targetBlogId = blog.id
const targetBlogIndex = currentBlogList.findIndex(blog => blog.id === targetBlogId)
blog.likes = blog.likes + 1
const updatedBlog = await blogService.like(blog)
currentBlogList[targetBlogIndex] = updatedBlog
setBlogs(currentBlogList)
}
const blogBrief = () => (
<>
{blog.title} {blog.author}
<button onClick={toggleDetail}>view</button>
</>
)
const blogDetail = () => (
<>
{blog.title} <button onClick={toggleDetail}>hide</button><br />
{blog.url}<br />
likes {blog.likes} <button onClick={() => incrementLike(blog)}>like</button><br />
{blog.author}<br />
</>
)
return (
<li className="blog_style">
{showDetail ? blogDetail() : blogBrief()}
</li>
)
}
色々書いてありますが、今回問題が起こったのは incrementLike の関数でした。
この関数はblogのlikeの値をインクリメントする関数で、画面上だと "like" のボタンをクリックすることで発火します。
サーバーから更新されたblogオブジェクトが返ってきたらblogList内の該当する要素と置き換えて、setBlogs関数にその配列を渡します。そうすることでblogsが更新されて画面がre-renderされ、画面上のlikeの数が変わるというわけです。
likeボタンを押すと確かにlikeの値はしっかり変更されるのですが、画面上ではlikeの数はそのままでした。
つまり、re-renderされていませんでした。
一旦デバッグしてみる
とりあえずconsole.logしまくります。
const incrementLike = async blog => {
const currentBlogList = blogList
console.log('current blogs:',currentBlogList)
const targetBlogId = blog.id
console.log('target blog id:',targetBlogId)
const targetBlogIndex = currentBlogList.findIndex(blog => blog.id === targetBlogId)
console.log('targ blog index:', targetBlogIndex)
blog.likes = blog.likes + 1
const updatedBlog = await blogService.like(blog)
console.log('updated blog:',updatedBlog)
currentBlogList[targetBlogIndex] = updatedBlog
setBlogs(currentBlogList)
}
で、この要素を更新してみます
現在likesの値は6なので、一度likeをクリックすれば7になるはずです。
↓
ちゃんと7になってます。
ただ、なんか元の配列内の要素も同じく更新されてしまっています。参照渡しになってるっぽい?
意図していた挙動ではなかったので一旦Arrayでググってみる。
Arrays are a special type of objects. The typeof operator in JavaScript returns "object" for arrays.
ArrayはObjectらしいです。
Objectということは参照渡しです。つまり、BlogコンポーネントのincrementLikeでcurrentBlogListを更新するということは、もとを辿ってゆくとAppコンポーネントのblogsを更新しているのと同じということです。
ただ、同じ値であったとしてもblogsを更新するsetBlogsに値を渡しているのだからre-renderされるのでは? という考えが払拭できなかったので、とりあえず公式ドキュメントを読み直してみました。
同じ値で更新を行った場合re-renderされない
現在値と同じ値で更新を行った場合、React は子のレンダーや副作用の実行を回避して処理を終了します(React は Object.is による比較アルゴリズムを使用します)。
らしいです。
つまり
const incrementLike = async blog => {
const currentBlogList = blogList
console.log('current blogs:',currentBlogList)
const targetBlogId = blog.id
console.log('target blog id:',targetBlogId)
const targetBlogIndex = currentBlogList.findIndex(blog => blog.id === targetBlogId)
console.log('targ blog index:', targetBlogIndex)
blog.likes = blog.likes + 1
const updatedBlog = await blogService.like(blog)
console.log('updated blog:',updatedBlog)
currentBlogList[targetBlogIndex] = updatedBlog
console.log('are blogList and currentBloglist the same object?:', Object.is(blogList, currentBlogList))
setBlogs(currentBlogList)
}
こういうことなので画面はそのままだったということのようです。
修正
const incrementLike = async blog => {
const currentBlogList = [...blogList]
const targetBlogId = blog.id
const targetBlogIndex = currentBlogList.findIndex(blog => blog.id === targetBlogId)
blog.likes = blog.likes + 1
const updatedBlog = await blogService.like(blog)
currentBlogList[targetBlogIndex] = updatedBlog
setBlogs(currentBlogList)
}