LoginSignup
1
0

More than 3 years have passed since last update.

Reactのstate hookに値を変更した配列を渡してもre-renderされなかった

Posted at

タイトル通りです。
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)
}

で、この要素を更新してみます
スクリーンショット 2021-03-24 23.31.57.png
現在likesの値は6なので、一度likeをクリックすれば7になるはずです。

スクリーンショット 2021-03-24 23.54.44.png

ちゃんと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)
}


スクリーンショット 2021-03-25 0.29.00.png

こういうことなので画面はそのままだったということのようです。

修正

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)
}
1
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0