Search code examples
react-reduxredux-thunk

why is an array fetched from backend not in the same order in redux store (react app)?


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:

  • how come the array fetched is not in the same order in redux store?
  • why does the UI flash the "wrong" order for a sec, then the correct order? how does it know the correct order if those 4 posts are still at the top in the store?

i'm confused here, any hint or help is appreciated! thanks


Solution

  • 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 ;)