Actions
import { FETCH_BLOG, FETCH_BLOG_ERROR, FETCH_BLOG_LOADING } from "../constants/blogActionTypes" const initialState = { blogs: [], error: '', loading: false, allBlogs: [] } // eslint-disable-next-line import/no-anonymous-default-export export default (blogs = initialState, action) => { switch (action.type) { case FETCH_BLOG_LOADING: return { blogs: [...blogs.blogs], loading: true, error: '' }; case FETCH_BLOG_ERROR: return { blogs: [...blogs.blogs], loading: false, error: action.payload }; case FETCH_BLOG: return { blogs: [...action.payload, ...blogs.blogs], loading: false, error: '' }; default: return blogs; } }
Reducers
export const fetchBlogs = (data) => async (dispatch) =>{ dispatch({ type: FETCH_BLOG_LOADING, payload: true }) fetch('http://localhost:5000/blog?show=' + data, { method: 'GET', headers: { authorization: userData.token } }) .then(res => res.json()) .then(data => { if (data.message) { dispatch(fetchBlogsError(data.message)) } else { dispatch({ type: FETCH_BLOG, payload: data }) } }) }
React
const [fetchData, setFetchData] = useState(0); const showData = () => { setFetchData(fetchData + 10) } const dispatch = useDispatch(); const { loading, error, blogs, } = useSelector(state => state.blogs) const getData = useCallback( () => { dispatch(fetchBlogs(fetchData)) }, [fetchData]) useEffect(() => { getData() }, [getData])
On the first render, I fetch 10 items.after clicking on load more I fetch another 10 data from database. On the blog component it’s fine but after go back to the home page and get back to the blog page; the blog items duplicates. How to fix this duplicate issue>
Answer
There are two issues here which are inter-related, you possibly don’t need to address #2 depending on how you address #1.
- You should add a condition to your thunk action so that you don’t fetch a page that you have previously fetched.
- You should separate your blog items by page so that you aren’t always appending the newest items at the end of the array if you fetch page 1 twice.
Sidenote: [...blogs.blogs]
is unnecessary because there is reason to clone properties which you aren’t changing.
I’m confused by your API calls. It looks like /blog?show=20
is getting posts 21-30 but I would think based on the name show
that it would be posts 1-20.
Using position indexes:
import { createAsyncThunk, createReducer } from "@reduxjs/toolkit"; export const fetchBlogs = createAsyncThunk( "blogs/fetchBlog", async (startIndex, { getState, rejectWithValue }) => { const res = await fetch("http://localhost:5000/blog?show=" + startIndex, { method: "GET", headers: { // where does userData come from ?? authorization: userData.token } }); const data = await res.json(); if (data.message) { rejectWithValue(data.message); } else { return data; } }, { condition: (startIndex, { getState }) => { const { blogs } = getState(); // cancel if loading of if first post on paage is loaded if (blogs.loading || blogs.blogs[startIndex]) { return false; } } } ); const initialState = { blogs: [], error: "", loading: false }; export default createReducer(initialState, (builder) => builder .addCase(fetchBlogs.pending, (state) => { state.loading = true; state.error = ""; }) .addCase(fetchBlogs.rejected, (state, action) => { state.loading = false; state.error = action.payload ?? action.error; }) .addCase(fetchBlogs.fulfilled, (state, action) => { const startIndex = action.meta.arg; const newBlogs = action.payload; // insert in the array at the correct position state.blogs.splice(startIndex, newBlogs.length, newBlogs); }) );
Using separated pages:
import { createAsyncThunk, createReducer, createSelector } from "@reduxjs/toolkit"; export const fetchBlogs = createAsyncThunk( "blogs/fetchBlog", async (pageNumber, { getState, rejectWithValue }) => { const startIndex = 10 * (pageNumber - 1); const res = await fetch("http://localhost:5000/blog?show=" + startIndex, { method: "GET", headers: { // where does userData come from ?? authorization: userData.token } }); const data = await res.json(); if (data.message) { rejectWithValue(data.message); } else { return data; } }, { condition: (pageNumber, { getState }) => { const { blogs } = getState(); // cancel if loading of if there is a property for this page if (blogs.loading || blogs.blogs[pageNumber]) { return false; } } } ); const initialState = { //arrays keyed by page number blogs: {}, error: "", loading: false }; export default createReducer(initialState, (builder) => builder .addCase(fetchBlogs.pending, (state) => { state.loading = true; state.error = ""; }) .addCase(fetchBlogs.rejected, (state, action) => { state.loading = false; state.error = action.payload ?? action.error; }) .addCase(fetchBlogs.fulfilled, (state, action) => { const pageNumber = action.meta.arg; state.blogs[pageNumber] = action.payload; }) ); // want to flatten the blogs array when selecting // create a memoized selector export const selectBlogs = createSelector( state => state.blogs, (blogsState) => ({ ...blogsState, blogs: Object.values(blogsState.blogs).flat(1) }) )
With component:
export default () => { const [pageNumber, setPageNumber] = useState(1); const showNext = () => { setPageNumber((page) => page + 1); }; const dispatch = useDispatch(); const { loading, error, blogs } = useSelector(selectBlogs); useEffect(() => { dispatch(fetchBlogs(pageNumber)); }, [dispatch, pageNumber]);