You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js状态更新触发页面软刷新问题求助

问题分析与解决方案

核心问题

你遇到的状态重置、整页重渲染问题,主要来自四个关键错误:

  1. 客户端组件使用async声明:Next.js中"use client"标记的组件不能是异步函数,这会导致状态更新时触发服务器端重新渲染,直接重置所有客户端状态。
  2. 内部组件重复创建:ReviewWizard组件定义在CreateReview内部,每次父组件状态更新都会重新创建这个组件,引发额外重渲染。
  3. 状态更新的闭包陷阱:onChecked中直接使用当前html状态快照,而非函数式更新,可能导致状态更新不及时,同时触发不必要的组件重渲染。
  4. Hook调用时机错误:在onSubmit事件处理函数中直接调用useFetch,违反了React Hook必须在组件顶层调用的规则,会导致不可预测的渲染行为。

修复步骤

1. 移除async,改为普通客户端组件

"use client"组件不能是异步函数,把组件声明改为普通函数:

export default function CreateReview({
    userProp,
    type,
    currentUserProp,
    existingReviewProp,
    isSelf,
}: {
    userProp: string;
    type: string;
    currentUserProp: string;
    existingReviewProp: string;
    isSelf: boolean;
}) {
    const existingReview: Review = JSON.parse(existingReviewProp);
    const currentUser: User = JSON.parse(currentUserProp);
    const user: User = JSON.parse(userProp);
    // ... 其余代码不变
}

2. 将ReviewWizard移到组件外部

避免每次父组件渲染都重新创建该组件,同时通过props传递所需数据:

// 移到CreateReview组件外部
const ReviewWizard = ({ stars, onChecked }: { stars: number, onChecked: (e: any, text: string, id: string) => void }) => {
    return (
        <Accordion.Root
            className="AccordionRoot"
            type="single"
            defaultValue="item-1"
            collapsible
        >
            <Accordion.Item className="AccordionItem" value="item-1">
                <Accordion.Trigger>
                    Review Wizard (Beta)
                    <span className="toggle-open"> Toggle Open</span>
                </Accordion.Trigger>
                <Accordion.Content>
                    {stars === 5 ? (
                        <ul>
                            <li>
                                <input
                                    type="checkbox"
                                    onClick={(e) =>
                                        onChecked(
                                            e,
                                            `<span id="great-work-with">Great to work with! </span>`,
                                            "great-work-with"
                                        )
                                    }
                                />
                                <span>Great to work with! </span>
                            </li>
                            {/* ... 其余5星选项 */}
                        </ul>
                    ) : null}
                    {/* ... 其余星级的判断逻辑 */}
                </Accordion.Content>
            </Accordion.Item>
        </Accordion.Root>
    );
};

// 在CreateReview组件内渲染时传递props
export default function CreateReview(...) {
    // ... 内部代码
    return (
        <Dialog.Root>
            {/* ... 其余内容 */}
            <Dialog.Content className="DialogContent standard-dialog">
                {currentUser.isBetaUser ? <ReviewWizard stars={stars} onChecked={onChecked} /> : null}
                {/* ... 其余内容 */}
            </Dialog.Content>
        </Dialog.Root>
    )
}

3. 使用函数式更新状态

修改onChecked和onChange中的状态更新逻辑,确保拿到最新的状态值,避免闭包问题:

const onChange = (e) => {
    setHtml(prev => sanitizeHtml(e.target.value));
};

const onChecked = (e: any, text: string, id: string) => {
    if (e.target.checked) {
        setHtml(prev => prev + text);
    } else {
        const htmlRegex = new RegExp(
            `<span\\s+id=['"]${id}['"][^>]*>.*?</span>`,
            "g"
        );
        setHtml(prev => prev.replace(htmlRegex, ""));
    }
};

4. 修复useFetch的调用方式

React Hook不能在事件处理函数中调用,调整useFetch为返回可调用函数的形式:

// 调整useFetch实现,返回fetchData函数
function useFetch() {
    const [data, setData] = useState(null);
    const [error, setError] = useState("");
    const [loading, setLoading] = useState(false);
    const [deleyed, setDeleyed] = useState(false);
    const [aborted, setAborted] = useState(false);

    useEffect(() => {
        if (!error && !loading && data) {
            window.location.reload();
        }
    }, [data, error, loading]);

    const fetchData = async (options: any) => {
        if (aborted) return;
        setLoading(true);
        setError("");
        try {
            const res = await fetch(options.url, {
                method: options.method,
                body: JSON.stringify(options.body),
                headers: { "Content-Type": "application/json" }
            });
            const result = await res.json();
            setData(result);
        } catch (err) {
            setError(err instanceof Error ? err.message : "Request failed");
        } finally {
            setLoading(false);
        }
    };

    return { fetchData, data, error, loading, deleyed, aborted, setAborted };
}

// 在CreateReview组件中使用
export default function CreateReview(...) {
    const { fetchData, data, error, loading, deleyed, aborted, setAborted } = useFetch();

    const onSubmit = async (e: any) => {
        e.preventDefault();
        fetchData({
            url: "/api/reviews/add",
            method: HTTPMethod.POST,
            body: {
                title: titleRef.current.value,
                stars: stars,
                text: html,
                type: type,
                author: currentUser._id,
                recipient: user._id,
                status: "unlisted",
            },
        });
    };

    // ... 其余代码
}

额外优化

  • 用useCallback包裹事件处理函数,避免传递给子组件时触发不必要重渲染:
const onChange = useCallback((e) => {
    setHtml(prev => sanitizeHtml(e.target.value));
}, []);

const onChecked = useCallback((e: any, text: string, id: string) => {
    // ... 函数内容
}, []);
  • 用useMemo处理ReviewWizard的props,进一步减少重渲染:
const wizardProps = useMemo(() => ({
    stars,
    onChecked
}), [stars, onChecked]);

// 渲染时
{currentUser.isBetaUser ? <ReviewWizard {...wizardProps} /> : null}

内容的提问来源于stack exchange,提问作者Ethan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 11:42:34