使用 reactjs 从 api 缓存数据
Cache data from api with reactjs
我最近才开始学习 React,我遇到了缓存数据的问题。这是我的想法,不知道对不对。抱歉,英语不好,感谢您的帮助。
我写了一个钩子,允许在以 web url 作为键调用 api 后缓存数据,如果 web 再次调用 api 他们将 return 缓存数据而不是再次调用 api。
我使用 会话存储 因为它不在浏览器选项卡之间共享数据,问题是我认为它会在页面刷新 (f5) 时丢失。一切正常,除了刷新网页时无法从 api 获取最新数据(因为数据是从会话存储中获取的)。
问题:
- 使用会话存储来缓存数据是否正确?
- 如果为真,那么我应该怎么做才能在刷新网页时,数据将来自 api 而不是会话存储。
- 如果错误那么缓存数据的正确方法是什么?
目标:
- [x] 在第一次访问时,数据来自 api(之后数据写入缓存)。
- [x]下次再来。来自缓存的数据(如果有)。
- [ ] 刷新时,数据来自 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.
处理
流量为:
- 检查 在 reducer_state.cached 中退出。
- 如果退出,通过退出状态调度reducer_action.setState。
- 如果不退出,dispatch thunk,将新状态推送到extraReducer句柄中缓存。
通过这种方式,它满足了我想要的一切:
- 首次访问时从 api 加载数据。
- 退出时从缓存中加载数据。
- 清除缓存,刷新或新页面时从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;
我最近才开始学习 React,我遇到了缓存数据的问题。这是我的想法,不知道对不对。抱歉,英语不好,感谢您的帮助。
我写了一个钩子,允许在以 web url 作为键调用 api 后缓存数据,如果 web 再次调用 api 他们将 return 缓存数据而不是再次调用 api。
我使用 会话存储 因为它不在浏览器选项卡之间共享数据,问题是我认为它会在页面刷新 (f5) 时丢失。一切正常,除了刷新网页时无法从 api 获取最新数据(因为数据是从会话存储中获取的)。
问题:
- 使用会话存储来缓存数据是否正确?
- 如果为真,那么我应该怎么做才能在刷新网页时,数据将来自 api 而不是会话存储。
- 如果错误那么缓存数据的正确方法是什么?
目标:
- [x] 在第一次访问时,数据来自 api(之后数据写入缓存)。
- [x]下次再来。来自缓存的数据(如果有)。
- [ ] 刷新时,数据来自 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.
流量为:
- 检查 在 reducer_state.cached 中退出。
- 如果退出,通过退出状态调度reducer_action.setState。
- 如果不退出,dispatch thunk,将新状态推送到extraReducer句柄中缓存。
通过这种方式,它满足了我想要的一切:
- 首次访问时从 api 加载数据。
- 退出时从缓存中加载数据。
- 清除缓存,刷新或新页面时从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;