React自定义Hook状态丢失求助:如何保留已有调用数据
解决React自定义Hook中状态被重置的问题
问题场景
在自定义Hook中发起多个接口调用时,后续调用会覆盖之前已更新的状态。例如调用selectTicket后selectedTicket有值,但调用fetchComments后selectedTicket被重置为初始的null。
代码示例
基础状态Hook useApiState
export const createInitialState = <T, E = Error>( initialData: T, withPagination = false, initialPaginationParams?: Partial<OffsetPaginationParams> ): ApiState<T, E> => ({ data: initialData, loading: false, error: null, ...(withPagination ? { pagination: { ...DEFAULT_PAGINATION_PARAMS, ...initialPaginationParams, }, } : {}), }); export function apiStateReducer<T, E = Error>( state: ApiState<T, E>, action: ApiAction<T, E> ): ApiState<T, E> { switch (action.type) { case "REQUEST": return { ...state, loading: true, error: null, }; case "SUCCESS": const newData = typeof action.payload === "function" ? (action.payload as (prev: T) => T)(state.data) : action.payload; return { ...state, data: newData, loading: false, error: null, }; case "FAILURE": return { ...state, loading: false, error: action.error, }; case "SET_PAGINATION_PARAMS": if (!state.pagination) { return state; } return { ...state, pagination: { ...state.pagination, ...action.params, }, }; case "RESET": return createInitialState<T, E>(state.data, !!state.pagination); default: return state; } } interface UseApiStateOptions { withPagination?: boolean; initialPaginationParams?: Partial<PaginationParams>; } export function useApiState<T, E = Error>( initialData: T | null = null, options: UseApiStateOptions = {} ) { const [state, dispatch] = useReducer( apiStateReducer<T, E>, createInitialState<T, E>( initialData, options.withPagination, initialPaginationParams ) ); const setLoading = useCallback(() => { dispatch({ type: "REQUEST" }); }, []); const setData = useCallback((data: T | ((prev: T) => T)) => { dispatch({ type: "SUCCESS", payload: data }); }, []); const setError = useCallback((error: E) => { dispatch({ type: "FAILURE", error }); }, []); const setPaginationParams = useCallback( (params: Partial<PaginationParams>) => { dispatch({ type: "SET_PAGINATION_PARAMS", params }); }, [] ); const reset = useCallback(() => { dispatch({ type: "RESET" }); }, []); return { state, setLoading, setData, setError, setPaginationParams, reset, }; }
业务Hook useTickets
interface TicketData { tickets: Ticket[]; total: number; selectedTicket: Ticket | null; tasks: TicketTask[]; notes: TicketNote[]; conversations: TicketComment[]; activities: UserActivity[]; } const initialData: TicketData = { tickets: [], total: 0, selectedTicket: null, tasks: [], notes: [], conversations: [], activities: [], }; export function useTickets() { const { state, setLoading, setData, setError, setPaginationParams, reset } = useApiState<TicketData>(initialData, { withPagination: true, initialPaginationParams: { page: 1, pageSize: 20, }, }); const fetchComments = useCallback( async (ticketId: string) => { try { setLoading(); const result = await ticketService.listComments(ticketId); setData({ ...state.data!, conversations: result?.data ?? [], }); return result?.data ?? []; } catch (error) { console.error("Error fetching comments:", error); setError(error as Error); return []; } }, [setData, setError, setLoading] ); const selectTicket = useCallback( async (id: string) => { try { setLoading(); const ticket = await ticketService.getTicket(id); setData({ ...state.data!, selectedTicket: ticket ?? null, }); return ticket; } catch (error) { console.error("Error selecting ticket:", error); setError(error as Error); throw error; } }, [state.data] ); return { ...state.data, isLoading: state.loading, error: state.error, paginationParams: state.pagination, total: state.data.total, fetchComments, selectTicket, setPaginationParams, reset, }; }
组件代码
export const TicketDetailsPage = () => { const { ticketId } = useParams(); const isInitialLoadRef = useRef(true); const { selectedTicket, conversations = [], activities = [], tasks = [], notes = [], isLoading, selectTicket, fetchComments, } = useTickets(); useEffect(() => { const loadData = async () => { if (!ticketId) return; try { console.log("Loading data for ticket:", ticketId); await selectTicket(ticketId); await Promise.all([ fetchComments(ticketId), ]); console.log("所有数据加载完成"); } catch (error) { console.error("加载工单数据出错:", error); } }; if (isInitialLoadRef.current && ticketId) { loadData(); isInitialLoadRef.current = false; } }, [ ticketId, selectTicket, fetchComments, ]); // 剩余组件代码... };
问题原因
- 闭包捕获旧值:
fetchComments中使用的state.data是闭包捕获的初始状态值,因为useCallback的依赖数组没有包含state.data,函数创建后一直引用旧值,展开后会覆盖已更新的selectedTicket。 - 不必要的函数重创建:
selectTicket的依赖数组包含state.data,导致每次状态变化都会重新创建该函数,进而触发组件useEffect重复执行。
修复方案
利用useApiState reducer已支持的函数式更新特性,在setData中传入函数获取最新状态,避免依赖闭包旧值,同时修正useCallback的依赖数组。
修改后的useTickets关键代码
export function useTickets() { const { state, setLoading, setData, setError, setPaginationParams, reset } = useApiState<TicketData>(initialData, { withPagination: true, initialPaginationParams: { page: 1, pageSize: 20, }, }); const fetchComments = useCallback( async (ticketId: string) => { try { setLoading(); const result = await ticketService.listComments(ticketId); // 函数式更新获取最新状态 setData(prevData => ({ ...prevData!, conversations: result?.data ?? [], })); return result?.data ?? []; } catch (error) { console.error("Error fetching comments:", error); setError(error as Error); return []; } }, [setData, setError, setLoading] ); const selectTicket = useCallback( async (id: string) => { try { setLoading(); const ticket = await ticketService.getTicket(id); // 函数式更新确保基于最新状态修改 setData(prevData => ({ ...prevData!, selectedTicket: ticket ?? null, })); return ticket; } catch (error) { console.error("Error selecting ticket:", error); setError(error as Error); throw error; } }, [setData, setError, setLoading] ); // 剩余代码不变... }
原理说明
函数式更新会让reducer传入当前最新的state.data作为参数,确保每次更新都基于最新状态,完全避免闭包捕获旧值的问题。同时,useCallback的依赖数组只包含稳定的setData/setError/setLoading(这些函数由useCallback缓存,不会随状态变化重新创建),保证了函数稳定性,避免不必要的重渲染。
内容的提问来源于stack exchange,提问作者King
相关产品推荐
相关产品推荐

