In my React app, i am fetching an array of posts from a backend api (nodejs/SQL DB). I am using redux for the frontend, so i thought it would be a good idea to sort the posts on the backend and send them to the frontend (sorted by id, from latest to oldest). Then, the array of posts gets stored in my redux store.
It's working fine, but i am confused because when i check the store, the posts are not ordered anymore, or rather: the same 4 random posts always get "pushed" to the top and then the rest is ordered as i wanted. So when i refresh the page i can see these older random posts in the UI at the top of the thread/feed of posts and when component is fully mounted it renders posts in the correct order. Not good.
I wanted to avoid sorting the array of posts on the frontend for performance concerns, am i wrong?
Redux initial state:
const initialState = {
posts: [],
userPosts: [],
currentPost: {
title: "",
text: "",
imgUrl: "",
},
scrapedPost: {},
comments: [],
replies: [],
likes: [],
error: "",
lastPostAdded: null,
lastReplyAdded: null,
lastDeleted: null,
sessionExpired: false,
users: [],
};
Redux root reducer:
import { combineReducers } from "redux";
import { postsReducer } from "./posts.reducer.js";
import { userReducer } from "./user.reducer.js";
export const rootReducer = combineReducers({
user: userReducer,
posts: postsReducer,
});
Redux store config:
import { applyMiddleware, createStore } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import { persistReducer, persistStore } from "redux-persist";
import autoMergeLevel2 from "redux-persist/lib/stateReconciler/autoMergeLevel2";
import storage from "redux-persist/lib/storage";
import thunk from "redux-thunk";
import { rootReducer } from "./reducers/root.reducer";
const composeEnhancer = composeWithDevTools({ trace: true, traceLimit: 25 });
const persistConfig = {
key: "root",
storage,
stateReconciler: autoMergeLevel2,
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
const store = createStore(persistedReducer, composeEnhancer(applyMiddleware(thunk)));
const persistor = persistStore(store);
export { store, persistor };
getPost action creator (using thunk middleware for async task):
export const getPosts = () => async (dispatch) => {
const accessToken = localStorage.getItem("jwt");
const request = {
headers: {
"Access-Control-Allow-Origin": "*",
Authorization: `Bearer ${accessToken}`,
},
method: "get",
};
try {
const response = await fetch(API_POST, request);
const data = await response.json();
const { posts, likes, sessionExpired } = data;
if (sessionExpired) {
dispatch({ type: SESSION_EXPIRED, payload: sessionExpired });
return;
}
dispatch({ type: GET_POSTS, payload: { posts, likes } });
} catch (error) {
dispatch({ type: SET_ERROR_POST, payload: error.message });
}
}
the posts reducer:
export const postsReducer = (state = initialState, action) => {
switch (action.type) {
case GET_POSTS: {
const { posts, likes } = action.payload;
return { ...state, posts, likes };
}
case GET_LIKES: {
const { likes } = action.payload;
return { ...state, likes };
// all other actions...//
}
relevant part of the UI code (feed component):
const Feed = () => {
const [newUser, setNewUser] = useState(false);
const user = useSelector((state) => state.user);
const { isAuthenticated, isNewUser } = useSelector((state) => state.user);
const posts = useSelector((state) => state.posts.posts);
const dispatch = useDispatch();
const userLanguage = useLanguage();
useEffect(() => {
window.scrollTo(0, 0);
setNewUser(isNewUser);
return function cleanup() {
setNewUser(null);
};
}, [isNewUser]);
useEffect(() => {
dispatch(getPosts());
}, []);
return (
<Layout>
//some jsx...//
<button className="h-6 refreshBtn outline-none hover:cursor-pointer bg-blue-500
text-white rounded-full gap-1 flex items-center justify-center pl-2 pr-3 py-1
shadow transition-all duration-300 hover:bg-black hover:shadow-none group"
onClick={() => dispatch(getPosts())}
style={{ opacity: posts && posts.length !== 0 ? 1 : 0 }}>
<RefreshIcon className="h-4 w-4 pointer-events-auto transform transition
transform duration-500 group-hover:-rotate-180" />
<span className="text-xs pointer-events-auto capitalize">
{userLanguage?.feed.refreshBtn}</span>
</button>
<div className="posts-wrapper h-full w-full relative flex flex-col items-center
justify-center gap-4 pb-6">
{posts.length === 0
? (<Skeleton element="post" number={8} />)
: (posts.map((post) => <Post key={post.postId} post={post} />)}
</div>
</Layout>
};
posts ordered by Id on the backend: screenshot
posts in the redux store (as you can see by their postId, indexes 0 to 3 have nothing to do there) screenshot
so my questions:
i'm confused here, any hint or help is appreciated! thanks
I finally found the solution months ago but forgot to come back here to give the solution to the issue i had.
Turns out the order of the posts fetched from backend wasn't modified or messed up with by Redux at all but by me (of course!) from another component called PopularPosts
.
Consider the code below:
const PopularPosts = () => {
const { posts } = useSelector(state => state.posts);
const [top3, setTop3] = useState<IPost[]>([]);
useEffect(() => {
setTop3(posts.sort((a, b) => { my sorting logic }).splice(0, 3));
}, [posts]);
I was literally mutating the store directly in order to create my top3. Of course this was a HUGE mistake! I should have used the sort()
method on a COPY of the store, not the store itself.
Here is the correct code:
const PopularPosts = () => {
const { posts } = useSelector(state => state.posts);
const [top3, setTop3] = useState<IPost[]>([]);
useEffect(() => {
const postsCopy = [...posts];
setTop3(postsCopy.sort((a, b) => { // my sorting logic }).splice(0, 3));
}, [posts]);
All is working as intended since this correction.
And lesson learnt: i'll never mutate the Redux store directly ever again ;)