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

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,
    ]);
    // 剩余组件代码...
};

问题原因

  1. 闭包捕获旧值:fetchComments中使用的state.data是闭包捕获的初始状态值,因为useCallback的依赖数组没有包含state.data,函数创建后一直引用旧值,展开后会覆盖已更新的selectedTicket。
  2. 不必要的函数重创建: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 11:00:55