React – useEffect带有依赖项仍然在第一次渲染时触发

huangapple go评论54阅读模式
英文:

React - useEffect with dependency still fires on first render

问题

你的组件为什么在触发第二个Effect钩子时不等待点击按钮事件?

BlogPost.jsx

import React from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useState, useEffect } from "react";
import { Footer } from "../components/Footer";

export const BlogPost = () => {
    const [blogData, setBlogData] = useState({title: null, content: null, tags: null}); // 初始化状态,以便在useEffect获取数据之前显示 "null"。
    const [deletePost, setDeletePost] = useState(false); // 初始化标志 "现在是时候删除帖子了"。

    const navigate = useNavigate();

    const urlParams = useParams();

    /* 第一个钩子在挂载后立即触发,检索博客文章的数据 */
    useEffect(() => {

        const retrieveBlog = async () => {
            // 我们需要使用博客标题向restdb.io发出查询,以检索内容和作者。

            const response = await fetch(`https://reactblogapp-52d2.restdb.io/rest/excel-blog?q={"title":"${urlParams.id}"}`,
                {
                    "headers": {
                        'Cache-Control': 'no-cache',
                        'x-apikey': '63d53b073bc6b255ed0c43c2',
                        'content-type': 'application/json'
                    }
                }
            );

            if (!response.ok) {
                console.log("数据库调用响应不正常!");
                navigate("/notfound");
            } else {
                const res = await response.json();
                setBlogData(res[0]);
            }
        }

        retrieveBlog().catch((error) => (console.log("出现错误:" + error)));

    }, []);

    /* 第二个effect钩子应在用户点击删除按钮时触发,但实际上在第一个钩子之后立即触发。 */
    useEffect(() => {

        const str = `https://reactblogapp-52d2.restdb.io/rest/excel-blog/${blogData._id}"`;
        console.log(str);

        console.log(blogData);

        const deleteFunction = async () => {
            const response = await fetch(str,
                {
                    "method": 'DELETE',
                    "headers":
                    {
                        'Cache-Control': 'no-cache',
                        'x-apikey': '63d53b073bc6b255ed0c43c2',
                        'content-type': 'application/json'
                    }
                });

            if (!response.ok) {
                console.log("数据库调用响应不正常!现在无法删除帖子。");
                navigate("/notfound"); // 钩子在用户点击按钮之前触发,blogData设置为undefined,因此它总是重定向到notfound,因为获取失败。
            } else {
                const res = await response.json();
                if (res["result"] === 0)
                    alert("现在无法删除此帖子!抱歉造成不便。");
            }
        }

        deleteFunction().catch((error) => (console.log("出现错误:" + error)));

    }, [deletePost]);


    return (
        <div>
            {!!blogData.title ? <h1>{blogData.title}</h1> : null}
            {!!blogData.content ? <p>{blogData.content}</p> : null}
            {!!blogData.tags ? <h5>{blogData.tags}</h5> : null}
            <button onClick={() => setDeletePost(true)}>删除此帖子</button>
            <Footer />
        </div>

    );
}

你的代码中有两个useEffect钩子,第一个在组件挂载后触发,第二个在deletePost状态变化时触发。问题可能是由于deletePost状态的更改导致第二个useEffect早于预期触发。你可以确保只有在点击"删除此帖子"按钮时才将deletePost状态设置为true,这样第二个useEffect只会在用户点击按钮时触发。如果它仍然在第一个useEffect之前触发,请确保没有其他地方更改了deletePost状态。

英文:

I'm having a weird issue in my component. I'm developing a blog app, and this specific component should display the selected blog post. In order to do that, on render it should fetch the data from a web database and display it. Then the component should provide a button that lets the user delete the specific post.

To achieve the above, I thought of using two useEffect hooks: the first triggers right after the mounting phase, ( empty dependencies []), the second triggers when the user clicks the button "Delete this post". The button has a onClick() event handler that switches the state variable deletePost to true; since the second useEffect hook has that state variable in its dependencies, it should trigger and send the fetch DELETE to the web db.

What happens, instead, is that the component gets mounted, the first useEffect fires but suddenly the second hook fires aswell, while blogData is set to undefined; the fetch DELETE fails and the user gets redirected to the error page.

I thought that it was a problem in the onClick event handler in the button definition, but I believe it's coded correctly.

BlogPost.jsx

import React from &quot;react&quot;;
import { useNavigate, useParams } from &quot;react-router-dom&quot;;
import { useState, useEffect } from &quot;react&quot;;
import { Footer } from &quot;../components/Footer&quot;;
export const BlogPost = () =&gt;
{
const [blogData, setBlogData] = useState({title : null, content : null, tags : null});  // Initialization of state, in order to display &quot;null&quot; until useEffect fetches the data.
const [deletePost, setDeletePost] = useState(false); // Initialization of the flag &quot;it&#39;s time to delete the post&quot;
const navigate = useNavigate();
const urlParams = useParams();
/* first hook fires right after mounting and it retrieves the blog post&#39;s data */
useEffect(() =&gt; {
const retrieveBlog = async () =&gt;
{
// We need to make a query to restdb.io with the blog title to retrieve content and author.
const response = await fetch(&quot;https://reactblogapp-52d2.restdb.io/rest/excel-blog?q={\&quot;title\&quot;:\&quot;&quot; + urlParams.id + &quot;\&quot;}&quot;,
{
&quot;headers&quot; : {
&#39;Cache-Control&#39; : &quot;no-cache&quot;,
&#39;x-apikey&#39;: &#39;63d53b073bc6b255ed0c43c2&#39;,
&#39;content-type&#39;: &#39;application/json&#39;
}
}
);
if (!response.ok)
{
console.log(&quot;Database call response not ok!&quot;);
navigate(&quot;/notfound&quot;);
}
else
{
const res = await response.json();
setBlogData(res[0]);
}
}
retrieveBlog().catch((error) =&gt; (console.log(&quot;There was an error: &quot; + error)));
}, []);
/* Second effect hook should fire when the user clicks on the delete button, it fires instead right after the first hook. */
useEffect(() =&gt; {
const str = &quot;https://reactblogapp-52d2.restdb.io/rest/excel-blog/&quot; + blogData._id + &quot;\&quot;}&quot;;
console.log(str);
console.log(blogData);
const deleteFunction = async () =&gt;
{
const response = await fetch(str,
{
&quot;method&quot; : &#39;DELETE&#39;,
&quot;headers&quot; : 
{
&#39;Cache-Control&#39; : &quot;no-cache&quot;,
&#39;x-apikey&#39;: &#39;63d53b073bc6b255ed0c43c2&#39;,
&#39;content-type&#39;: &#39;application/json&#39;
}
});
if (!response.ok)
{
console.log(&quot;Database call response not ok! Can&#39;t delete the post now.&quot;);
navigate(&quot;/notfound&quot;);  // hook fires BEFORE the user clicks on the button, blogData is set to undefined and it always redirects it to notfound, because the fetch fails.
}
else
{
const res = await response.json();
if (res[&quot;result&quot;]  === 0)
alert(&quot;Can&#39;t delete this post now! Sorry for the inconvenient.&quot;);
}
}
deleteFunction().catch((error) =&gt; (console.log(&quot;There was an error: &quot; + error)));
}, [deletePost]);
return(
&lt;div&gt;
{!!blogData.title ? &lt;h1&gt;{blogData.title}&lt;/h1&gt; : null}
{!!blogData.content ? &lt;p&gt;{blogData.content}&lt;/p&gt; : null}
{!!blogData.tags ? &lt;h5&gt;{blogData.tags}&lt;/h5&gt; : null}
&lt;button onClick={() =&gt; setDeletePost(true)}&gt;Delete this post&lt;/button&gt;
&lt;Footer /&gt;
&lt;/div&gt;
);
}

Why is my component not waiting for the onClick button event when firing the second Effect hook ?

答案1

得分: 1

> 但突然第二个钩子也被触发,同时blogData被设置为undefined;

你可以在第二个useEffect内部使用简单的条件来阻止函数被调用,例如:

if (!deletePost) return; 
或者
if (!blogData) return;

但这明显是一种变通方法。

> 我认为每次网络请求/ API 调用在组件的生命周期中被视为“副作用”,因此它应该放在 useEffect 钩子中。

API调用应该在useEffect钩子内部调用,但只有在你想在组件的特定状态(生命周期)下调用API时才需要,例如,如果你想在组件挂载时获取一些数据,你需要将它放在useEffect中。但是,删除调用不应该在组件状态改变时被调用。它应该在用户操作(事件)上被调用,即onClick

基本上,只需将删除调用应用到按钮上。

const handleDeletion = async () => {
const response = await fetch(str, ...);
};
&lt;button onClick={handleDeletion}&gt;删除这篇文章&lt;/button&gt;
英文:

> but suddenly the second hook fires aswell, while blogData is set to undefined;

You could e.g. prevent the function from being called with a simple condition inside the second useEffect, like:

if (!deletePost) return; 
or
if (!blogData) return;

but this is definitely a workaround.

> I thought that every web request / API call is considered a "side effect" in regards of the component's lifecycle, and thus it should be put in a useEffect hook

API calls should be called within useEffect hook, but only if you want to call the API in specified state (lifecycle) of the component, e.g. if you want to fetch some data on component mount, you need to place it in the useEffect. But the deletion call is not supposed to be called when the component state chages. It's supposed to be called on user-action (event), that is onClick.

Basically, just apply the deletion call to the button.

const handleDeletion = async () =&gt; {
const response = await fetch(str, ...);
};
&lt;button onClick={handleDeletion}&gt;Delete this post&lt;/button&gt;

huangapple
  • 本文由 发表于 2023年2月27日 18:12:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/75579131.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定