使用 reactjs 从 api 缓存数据

Cache data from api with reactjs

我最近才开始学习 React,我遇到了缓存数据的问题。这是我的想法,不知道对不对。抱歉,英语不好,感谢您的帮助。

我写了一个钩子,允许在以 web url 作为键调用 api 后缓存数据,如果 web 再次调用 api 他们将 return 缓存数据而不是再次调用 api。

我使用 会话存储 因为它不在浏览器选项卡之间共享数据,问题是我认为它会在页面刷新 (f5) 时丢失。一切正常,除了刷新网页时无法从 api 获取最新数据(因为数据是从会话存储中获取的)。

问题:

目标:

屏幕代码

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

挂钩:

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

我决定今天使用 redux-toolkit 来管理状态,它解决了我的问题。这个想法是创建具有 cached 的状态是一个在 reducer 中存储旧状态的数组,reducer 允许将旧状态设置为当前状态,extraReducer 从 api.

处理

流量为:

  1. 检查 在 reducer_state.cached 中退出。
  2. 如果退出通过退出状态调度reducer_action.setState
  3. 如果不退出dispatch thunk,将新状态推送到extraReducer句柄中缓存。

通过这种方式,它满足了我想要的一切:

  1. 首次访问时从 api 加载数据。
  2. 退出时从缓存中加载数据。
  3. 清除缓存,刷新或新页面时从api加载数据。

下面是我写的一些逻辑代码:

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;