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

React useEffect卸载时机异常:页面切换时Context状态未重置致API未调用

问题:页面切换时Context状态重置滞后导致新页面API未触发

场景与预期流程

我使用自定义IsEditedContext管理页面编辑状态,包含initial、edited、unedited三种状态,shouldRefresh字段根据status判断是否需要刷新数据。通过react-router-dom的<Link>实现页面导航,预期流程:

  • 导航至First页面:status为initial,shouldRefresh为true,调用API后执行markAsUnedited
  • 导航至Second页面:触发First页面卸载回调,重置status为initial,shouldRefresh设为true,调用Second页面API后执行markAsUnedited

实际问题

页面切换时,旧页面的useEffect卸载回调在新页面挂载完成后才触发,导致Context的status未及时重置,新页面的shouldRefresh仍为false,API无法调用。


现有代码

IsEditedContext代码

import { FC, PropsWithChildren, createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';

enum EditStatus {
  Initial = 'initial',
  Edited = 'edited',
  Unedited = 'unedited',
}

interface IsEditedContextType {
  shouldRefresh: boolean;
  status: EditStatus;
  markAsEdited: () => void;
  markAsUnedited: () => void;
  init: () => void;
}

const defaultContext: IsEditedContextType = {
  status: EditStatus.Initial,
  markAsEdited: () => {},
  markAsUnedited: () => {},
  shouldRefresh: false,
  init: () => {},
};

const IsEditedContext = createContext<IsEditedContextType>(defaultContext);

export const useIsEdited = (resetOnUnmount = false) => {
  const context = useContext(IsEditedContext);
  if (!context) {
    throw new Error('useIsEdited must be used within a IsEditedProvider');
  }
  if (resetOnUnmount) {
    useEffect(() => {
      return context.init;
    }, []);
  }
  return context;
};

export const IsEditedProvider: FC<PropsWithChildren> = ({ children }) => {
  const [status, setStatus] = useState<EditStatus>(EditStatus.Initial);

  const markAsEdited = useCallback(() => setStatus(EditStatus.Edited), []);
  const markAsUnedited = useCallback(() => setStatus(EditStatus.Unedited), []);
  const init = useCallback(() => setStatus(EditStatus.Initial), []);

  const shouldRefresh = useMemo(() => {
    return status === EditStatus.Initial || status === EditStatus.Edited;
  }, [status]);

  return (
    <IsEditedContext.Provider value={{ status, init, markAsEdited, markAsUnedited, shouldRefresh }}>
      {children}
    </IsEditedContext.Provider>
  );
};

页面组件代码(First/Second逻辑一致)

import { useEffect } from "react";
import { useIsEdited } from "../IsEditedContext";

const First = () => {
  const { shouldRefresh, markAsUnedited } = useIsEdited(true);
  useEffect(() => {
    if (shouldRefresh) {
      console.log('First page API call');
      markAsUnedited();
    };
  }, []);
  return ( <>First</> );
}

export default First;

解决方案

方案一:在Context Provider中监听路由变化(推荐)

直接在IsEditedProvider中监听路由路径变化,一旦路由切换就重置状态,确保新页面挂载前状态已更新。

修改IsEditedProvider代码

import { FC, PropsWithChildren, createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom'; // 新增导入

enum EditStatus {
  Initial = 'initial',
  Edited = 'edited',
  Unedited = 'unedited',
}

interface IsEditedContextType {
  shouldRefresh: boolean;
  status: EditStatus;
  markAsEdited: () => void;
  markAsUnedited: () => void;
  init: () => void;
}

const defaultContext: IsEditedContextType = {
  status: EditStatus.Initial,
  markAsEdited: () => {},
  markAsUnedited: () => {},
  shouldRefresh: false,
  init: () => {},
};

const IsEditedContext = createContext<IsEditedContextType>(defaultContext);

export const useIsEdited = () => { // 移除resetOnUnmount参数
  const context = useContext(IsEditedContext);
  if (!context) {
    throw new Error('useIsEdited must be used within a IsEditedProvider');
  }
  return context;
};

export const IsEditedProvider: FC<PropsWithChildren> = ({ children }) => {
  const [status, setStatus] = useState<EditStatus>(EditStatus.Initial);
  const location = useLocation(); // 获取当前路由

  const markAsEdited = useCallback(() => setStatus(EditStatus.Edited), []);
  const markAsUnedited = useCallback(() => setStatus(EditStatus.Unedited), []);
  const init = useCallback(() => setStatus(EditStatus.Initial), []);

  // 新增:路由切换时自动重置状态
  useEffect(() => {
    init();
  }, [location.pathname, init]);

  const shouldRefresh = useMemo(() => {
    return status === EditStatus.Initial || status === EditStatus.Edited;
  }, [status]);

  return (
    <IsEditedContext.Provider value={{ status, init, markAsEdited, markAsUnedited, shouldRefresh }}>
      {children}
    </IsEditedContext.Provider>
  );
};

修改页面组件代码

import { useEffect } from "react";
import { useIsEdited } from "../IsEditedContext";

const First = () => {
  const { shouldRefresh, markAsUnedited } = useIsEdited(); // 移除参数
  useEffect(() => {
    if (shouldRefresh) {
      console.log('First page API call');
      markAsUnedited();
    };
  }, [shouldRefresh, markAsUnedited]); // 添加依赖,确保状态变化时重新执行
  return ( <>First</> );
}

export default First;

方案二:使用useLayoutEffect同步执行卸载回调

利用useLayoutEffect的同步执行特性,确保旧页面的卸载回调在新页面挂载前触发。

修改useIsEdited代码

export const useIsEdited = (resetOnUnmount = false) => {
  const context = useContext(IsEditedContext);
  if (!context) {
    throw new Error('useIsEdited must be used within a IsEditedProvider');
  }
  if (resetOnUnmount) {
    useLayoutEffect(() => { // 替换useEffect为useLayoutEffect
      return context.init;
    }, []);
  }
  return context;
};

修改页面组件代码

import { useLayoutEffect } from "react"; // 替换useEffect为useLayoutEffect
import { useIsEdited } from "../IsEditedContext";

const First = () => {
  const { shouldRefresh, markAsUnedited } = useIsEdited(true);
  useLayoutEffect(() => {
    if (shouldRefresh) {
      console.log('First page API call');
      markAsUnedited();
    };
  }, [shouldRefresh, markAsUnedited]); // 添加依赖
  return ( <>First</> );
}

export default First;

内容的提问来源于stack exchange,提问作者XH栩恒

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 01:30:07