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栩恒
相关产品推荐
相关产品推荐

