Tuan
Tuan

Reputation: 2433

Cache data from api with reactjs

i just started learning react recently and i am facing a problem with caching data. this my idea, i don't know if it's the correct way. Sorry cause bad english and thanks for helps.

I wrote a hook that allows cache data after calling the api with web url as key, if the web call that api again they will return cached data instead of calling that api again.

I used session storage because it doesn't share data between browser tabs, the problem is that I thought it would lose on page refresh(f5). Everything is working fine except when refresh the web page it is not possible to get the latest data from the api (because the data is taken from session storage).

Question:

Target:

Screen code

...
const getter = {
  get offset(): number {
    return parseInt(searchParams.get("offset") ?? "0");
  },
  get limit(): number {
    return parseInt(searchParams.get("limit") ?? "20");
  },
};
const sessionCache = useSessionCache();
const [state, setState] = useState<PokesPageState>(initState);
const setStateOnly = (w: PokesPageState) => setState({ ...state, ...w });
useEffect(() => {
  // -> get data promise
  const getPokes = async (): Promise<PagingGeneric<Poke>> => {
    const path = location.pathname + location.search;
    return sessionCache.axiosPromise(path, () =>
      axios_apis.poke.getPokes(getter.offset, getter.limit)
    );
  };
  // -> set data to component state
  getPokes()
    .then((response) => setStateOnly({ pokes: response }))
    .catch((e) => console.log(e));
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [getter.limit, getter.offset]);
...

Hook:

export function useSessionCache() {
  const sessionCache = {
    get<T = any>(key: string): T | null {
      const value = sessionStorage.getItem(key);
      if (!value || value === "undefined") return null;
      return JSON.parse(value);
    },
    set<T = any>(key: string, value: T): T {
      sessionStorage.setItem(key, JSON.stringify(value));
      return value;
    },
    async axiosPromise<T>(
      key: string,
      promise: () => Promise<AxiosResponse<T>>
    ): Promise<T> {
      const value = sessionCache.get<T>(key);
      if (value !== null) return value;
      return await promise()
        .then((response) => response.data)
        .then((value) => sessionCache.set(key, value));
    },
  };

  return sessionCache;
}

Api:

const poke_api = {
  getPokes(offset: number, limit: number) {
    const url = `/pokemon?offset=${offset}&limit=${limit}`;
    return axios_request.get<PagingGeneric<Poke>>(url);
  },
  getPoke(noi: number | string) {
    const url = `/pokemon/${noi}`;
    return axios_request.get<PokeDetails>(url);
  },
};

enter image description here

Upvotes: 1

Views: 3643

Answers (1)

Tuan
Tuan

Reputation: 2433

I have decided to use redux-toolkit today to manage states and it solve my problem. The idea is create state that have cached is an Array storing old state in reducer, with an reducer allow set oldstate to current state, extraReducer to handle from api.

The flow is:

  1. Check have exits in reducer_state.cached.
  2. If exit, dispatch reducer_action.setState by exits state.
  3. If not exit, dispatch thunk, push new state in to cached in extraReducer handle.

By this way, it fulfills all the things that i want:

  1. Load data from api in firts access.
  2. Load data from cached when exits.
  3. Clear cached, load data from api when refresh or new page.

Below is some code of logic i was write:

reducer.ts

export interface PokesReducerState {
  pokes?: PagingGeneric<Poke>;
  limit: number;
  offset: number;
  error_msg?: string;
}
export interface PokesReducerCachedState extends PokesReducerState {
  cached: Array<PokesReducerState>;
}

const initialState: PokesReducerCachedState = {
  cached: [],
  limit: 20,
  offset: 0,
};

const pokes_reducer = createSlice({
  name: "pokes_reducer",
  initialState: initialState,
  reducers: {
    setState(state, { payload }: PayloadAction<PokesReducerState>) {
      if (!payload.error_msg) {
        state.pokes = payload.pokes;
        state.limit = payload.limit;
        state.offset = payload.offset;
        state.error_msg = payload.error_msg;
      }
    },
  },
  extraReducers: (builder) => {
    builder.addCase(pokes_thunks.get.pending, (state, action) => {
      state.limit = action.meta.arg.limit;
      state.offset = action.meta.arg.offset;
      state.error_msg = undefined;
      state.pokes = undefined;
    });
    builder.addCase(pokes_thunks.get.fulfilled, (state, action) => {
      state.pokes = action.payload;
      state.cached.push({
        limit: state.limit,
        offset: state.offset,
        pokes: state.pokes,
        error_msg: state.error_msg,
      });
    });
    builder.addCase(pokes_thunks.get.rejected, (state, action) => {
      state.error_msg = action.error.message;
      state.pokes = undefined;
    });
  },
});

export const pokes_actions = pokes_reducer.actions;
export default pokes_reducer;

index.ts

function PokesPage(props: PokesPageProps) {
  const [searchParams, setSearchParams] = useSearchParams();
  const dispatch = useAppDispatch();
  const selector = useSelector((state: RootState) => state.pokes_reducer);
  const getter = {
    get offset(): number {
      return parseInt(searchParams.get("offset") ?? "0");
    },
    get limit(): number {
      return parseInt(searchParams.get("limit") ?? "20");
    },
    ...
  };
  ...
  useEffect(() => {
    const cached_state = selector.cached.find(
      (cached) =>
        getter.offset === cached.offset && getter.limit === cached.limit
    );
    if (cached_state !== undefined) {
      console.log("found cached state...");
      dispatch(pokes_actions.setState(cached_state));
    } else {
      console.log("get new state...");
      dispatch(
        pokes_thunks.get({ offset: getter.offset, limit: getter.limit })
      );
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [getter.limit, getter.offset]);

  return (...);
}

export default PokesPage;

enter image description here

Upvotes: 1

Related Questions