Keeping state on a react hook

I am still learning react and I currently have an issue where I am trying to create custom hooks and have it make different calls but rentain value of calls already made. Currently, the calls are made but previous values get set back to default which is not what I want.I want to be able to retain previous data. If this is not possible I would like to get that information or other approach to it.

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,
            options.initialPaginationParams
        )
    );

    const setLoading = useCallback(() => {
        dispatch({ type: "REQUEST" });
    }, []);

    const setData = useCallback((data: 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,
    };
}

Then in my customHook

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,
            },
        });

        console.log("Current state in useTickets:", state.data);

    const fetchComments = useCallback(
        async (ticketId: string) => {
            console.log("Fetched comments PRE:", state.data);
            try {
                setLoading();
                const result = await ticketService.listComments(ticketId);
                console.log("Fetched comments:", result?.data);

                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);
                console.log("Fetched ticket:", ticket);

                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
        ...state.data,
        isLoading: state.loading,
        error: state.error,
        paginationParams: state.pagination,
        total: state.data.total,

        // Operations
        fetchComments,
        selectTicket,
        setPaginationParams,
        reset,
    };
}

Now in my Component

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);

                // Load ticket first
                await selectTicket(ticketId);

                // Load all related data in parallel
                await Promise.all([
                    fetchComments(ticketId),
                ]);

                console.log("All data loaded");
            } catch (error) {
                console.error("Error loading ticket data:", error);
            }
        };

        if (isInitialLoadRef.current && ticketId) {
            loadData();
            isInitialLoadRef.current = false;
        }
    }, [
        ticketId,
        selectTicket,
        fetchComments,
    ]);
    ...

So now when selectTicket gets called, I see the data but once fetchComments gets called, it automatically sets selectedTicket which should not be.

If you need more information kindly let me know. I appriciate every help possible.

Issue

The fetchComments callback has a Javascript Closure issue, in that it closes over the un-updated state value which is passed into the setData success action and unconditionally overwrites whatever the current state is.

Trivially you could add all the missing hook dependencies to the useCallback hook call.

const fetchComments = useCallback(
  async (ticketId: string) => {
    console.log("Fetched comments PRE:", state.data);
    try {
      setLoading();
      const result = await ticketService.listComments(ticketId);
      console.log("Fetched comments:", result?.data);

      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, state] // <-- add state as a dependency
);

But this only really pushes the Closure issue down stream. For example, now the useEffect hook call in TicketDetailsPage will get a new “copy” of the fetchComments, but the currently in-flight loadData fetching the data and dispatching actions to the useReducer hook won’t ever see the updated copy.

Solution Suggestion

Don’t try to pass any current state along with any new values you are wanting to add to the state. Handle merging all new state into the current state in the reducer cases. This avoids the stale closure issue of trying to manage this outside the reducers.

case "SUCCESS":
  const newData =
    typeof action.payload === "function"
      ? (action.payload as (prev: T) => T)(state.data)
      : action.payload;

    return {
      ...state,
      data: {
        ...state.data, // <-- shallow copy current data here!
        ...newData,    // <-- shallow merge new data here
      },
      loading: false,
      error: null,
    };

Now selectTicket and fetchComments need only pass the new data in the action, removing state as a useCallback hook dependency.

const selectTicket = useCallback(
  async (id: string) => {
    try {
      setLoading();
      const ticket = await ticketService.getTicket(id);
      console.log("Fetched ticket:", ticket);

      setData({
        selectedTicket: ticket ?? null,
      });

      return ticket;
    } catch (error) {
      console.error("Error selecting ticket:", error);
      setError(error as Error);
      throw error;
    }
  },
  [setData, setError, setLoading]
);

const fetchComments = useCallback(
  async (ticketId: string) => {
    console.log("Fetched comments PRE:", state.data);
    try {
      setLoading();
      const result = await ticketService.listComments(ticketId);
      console.log("Fetched comments:", result?.data);

      setData({
        conversations: result?.data ?? [],
      });

      return result?.data ?? [];
    } catch (error) {
      console.error("Error fetching comments:", error);
      setError(error as Error);
      return [];
    }
  },
  [setData, setError, setLoading]
);

Since it looks like you also typed your action payload to accept a callback function that is passed the current state.data value I suspect you could also implement a functional state update as well. Something like the following:

setData(data => ({
  ...data,
  selectedTicket: ticket ?? null,
}));
setData(data => ({
  ...state.data,
  conversations: result?.data ?? [],
}));

Between the two though the first is preferable since it doesn’t rely on downstream consumers of the state and handlers to know and maintain any part of your state invariant, e.g. it’s shape & properties. Complete and sole responsibility for updating the state rests fully on the case reducers.

5

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật