Search code examples
javascriptreactjsasynchronousreduxredux-thunk

Redux useSelect doesn't get updated as expected


I’m working on a little proof of concept project, using React and Redux, and useSelector and useDispatch hooks. I’m trying to fetch some data asynchronously and I use thunks for that. I think I'm conceptually missing something. Even though my state works as expected, I can not get my data from api using useSelector.

Here is the code. My action:

import axios from "axios";

export const API_FETCH_POSTS = 'API_FETCH_POSTS';

export const fetchPosts = (postId) => { // to simulate post request
    return (dispatch) => {

        let baseUrl = 'https://jsonplaceholder.typicode.com/';
        let postFix = 'comments?postId=';
        let url = `${baseUrl}${postFix}${postId}`;

        axios.get(url)
            .then(response => {
                const data = response.data;
                console.log(JSON.stringify(data)); // work!
                dispatch(fetchPostsSuccess(data));
            });
    }
};

const fetchPostsSuccess = posts => {
    return {
        type: API_FETCH_POSTS,
        payload: posts
    }
};

My reducer:

import {API_FETCH_POSTS} from "./apiActions";

const initialState = {
    getPostsReq : {
        posts: [],
    }
};

const apiReducer = (state = initialState, action) => {
    let getPostsReq;
    switch (action.type) {
        case API_FETCH_POSTS:
            getPostsReq = {
                posts: [...state.getPostsReq.posts]
            };

            return {
                ...state,
                getPostsReq
            };
        default: return state;
    }
};

export default apiReducer;

And rootReducer:

import {combineReducers} from 'redux';
import apiReducer from "./api/apiReducer";

export default combineReducers({
    api: apiReducer
})

And store:

const initialState = {};

const store = createStore(
    rootReducer,
    initialState,
    composeWithDevTools(
        applyMiddleware(thunk)
    )
);

export default store;

I have a problem with my React component:

function PostContainer(props) {
    const posts = useSelector(state => state.api.getPostsReq.posts);
    const dispatch = useDispatch();

    const logPosts = () => {
        {/*why doesn't this line work???*/}
        console.log(JSON.stringify(posts));
    }

    return (
        <div>
            <button onClick={() => {
                dispatch(fetchPosts(1));
                logPosts();
            }}>Fetch Posts</button>

            <div>
                {/*why doesn't this line work???*/}
                {posts.map(post => <p>{post.body}</p>)}
            </div>
        </div>
    );
}

export default PostContainer;

I expect that after I press the button, the function fetchPosts gets dispatched and because I use thunk I shouldn’t have any problems with asynchronicity. But by some reason I can’t get my state, using useSelector() hook. I can neither render the state, nor log it in the console.

What am I missing here?

Here is the whole code if it is more convenient - https://github.com/JavavaJ/use-select-problem


Solution

  • Problem: Not Storing Posts

    Your selector is fine, it's your reducer that's the problem! You dispatch an action which has an array of posts in the payload:

    const fetchPostsSuccess = posts => {
        return {
            type: API_FETCH_POSTS,
            payload: posts
        }
    };
    

    But when you respond to this action in the reducer, you completely ignore the payload and instead just return the same posts that you already had:

    const apiReducer = (state = initialState, action) => {
        let getPostsReq;
        switch (action.type) {
            case API_FETCH_POSTS:
                getPostsReq = {
                    posts: [...state.getPostsReq.posts]
                };
    
                return {
                    ...state,
                    getPostsReq
                };
            default: return state;
        }
    };
    

    Solution: Add Posts from Action

    You can rewrite your reducer like this to append the posts using Redux immutable update patterns.

    const apiReducer = (state = initialState, action) => {
      switch (action.type) {
        case API_FETCH_POSTS:
          return {
            ...state,
            getPostsReq: {
                ...state.getPostsReq,
                posts: [...state.getPostsReq.posts, ...action.payload]
            }
          };
        default:
          return state;
      }
    };
    

    It's a lot easier if you use Redux Toolkit! With the toolkit you can "mutate" the draft state in your reducers, so we don't need to copy everything.

    const apiReducer = createReducer(initialState, {
      [API_FETCH_POSTS]: (state, action) => {
        // use ... to push individual items separately
        state.getPostsReq.posts.push(...action.payload);
      }
    });