Reputation: 2433
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);
},
};
Upvotes: 1
Views: 3643
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:
By this way, it fulfills all the things that i want:
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;
Upvotes: 1