0

I created a very simple React-Redux App and fetching Users and Posts from https://jsonplaceholder.typicode.com/

In my components I am logging Users and Posts data into the console. As far as I see, in the network tab there is one request for Users and 10 requests for Posts. That's correct but in the console, I see 10 Posts requests for each User.

Does it mean ReactJS renders the component 100 times? What is my mistake in this code?

Any help will be greatly appreciated!

My code and codepen link are below

Please check the code in codepen

const { useEffect } = React;
const { connect, Provider } = ReactRedux;
const { createStore, applyMiddleware, combineReducers } = Redux;
const thunk = ReduxThunk.default;

//-- REDUCERS START -- //
const userReducer = (state = [], action) => {
  if (action.type === 'fetch_users') return [...action.payload];
  return state;
};
const postReducer = (state = [], action) => {
  if (action.type === 'fetch_posts') return [...action.payload];
  return state;
};
//-- REDUCERS END -- //

//-- ACTIONS START -- //
const fetchUsers = () => async dispatch => {
  const response = await axios.get(
    'https://jsonplaceholder.typicode.com/users'
  );
  dispatch({ type: 'fetch_users', payload: response.data });
};
const fetchPosts = userId => async dispatch => {
  const response = await axios.get(
    `https://jsonplaceholder.typicode.com/users/${userId}/posts`
  );
  
  dispatch({ type: 'fetch_posts', payload: response.data });
};
//-- ACTIONS END -- //

const reducer = combineReducers({ users: userReducer, posts: postReducer });
const store = createStore(reducer, applyMiddleware(thunk));

const mapStateToProps = state => {
  return { users: state.users, posts: state.posts };
};

const mapDispatchToProps = dispatch => {
  return {
    getUsers: () => dispatch(fetchUsers()),
    getPosts: (id) => dispatch(fetchPosts(id))
  };
};

const Users = props => {
  console.log('users', props.users);

  const { getUsers } = props;
  useEffect(() => {
    getUsers();
  }, [getUsers]);

  const renderUsers = () =>
    props.users.map(user => {
      return (
        <div>
          <div>{user.name}</div>
          <div>
            <PostsContainer userId={user.id} />
          </div>
        </div>
      );
    });
  return <div style={{backgroundColor:'green'}}>{renderUsers()}</div>;
};

const UserContainer = connect(mapStateToProps, mapDispatchToProps)(Users);

const Posts = props => {
  
 console.log('posts' , props.posts);

  const { getPosts, userId } = props;
  useEffect(() => {
    getPosts(userId);
  }, [getPosts, userId]);
  
  const renderPosts = () =>
    props.posts.map(post => {
      return (
        <div>
          <div>{post.title}</div>          
        </div>
      );
    });
  
  return <div style={{backgroundColor:'yellow'}}>{renderPosts()}</div>;
};

const PostsContainer = connect(mapStateToProps, mapDispatchToProps)(Posts);

const App = props => {
  return (
    <div>
      <UserContainer />
    </div>
  );
};

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
0

2 Answers 2

4
+50

Does it mean ReactJS renders the component 100 times? What is my mistake in this code?

  • you have a UserContainer, that renders and requests for users;
  • once fetched users, you have an update state. UserContainer rerenders, and now you have 10 PostContainers;
  • each PostContainer makes a request to fetch posts, 10 on total;
  • it results in 10 state updates. UserContainer rerenders 10 times, and each PostContainer rerenders 10 times;

The component doesn't renders 100 times, each PostContainer renders the initial mount then rerenders 10 times. since there are 10 PostContainers and each rerenders 10 times that's why you might think that renders 100 times.

you have some issues. the dependency issue, which was pointed out is the first. getUsers useEffect should have an empty dependency, and userId useEffect, should depend on userId.

to solve the 10 rerenders on UserContainer due to posts, you need to have a different mapStateToProps to each. for UserContainer you will map only users, otherwise you will get 10 updates due to 10 posts requests:

const mapUserStateToProps = state => {
  return { users: state.users };
};

with that it solves UserContainer 10 rerenders.

now about PostContainer there is something that needs to be fixed first, your reducer. your reducer replaces last posts with the current call. in the end you will have only the posts that arrived last, not all posts. to fix that you need to spread your state.

const postReducer = (state = [], action) => {
  if (action.type === 'fetch_posts') return [...state, ...action.payload];
  return state;
};

eventually, if in your project you could have a repeated request to same userId than it would be necessary to have some validation for not adding the same posts again

now it leads us to mapping props to PostContainer. you would need to have a filter on posts based on userId. mapStateToProps takes props as second argument, which enables us to accomplish that:

const mapPostStateToProps = (state, { userId }) => {
  return { posts: state.posts.filter(post => post.userId === userId) };
};

this looks the end to solve the issue, but each PostContainer still rerenders 10 times. why does this happens since posts will be the same? that happens because filter will return a new array reference, no matter if its content didn't change.

to solve this issue you can use React.memo. you need to provide the component and a equality function to memo. to compare an array of objects there are some solutions, also few libs that provide some deepEqual function. here I use JSON.stringify to compare, but you are free to use some other one:

const areEqual = (prevProps, nextProps) => {
  return JSON.stringify(prevProps.posts) === JSON.stringify(nextProps.posts)
}

you would validate also other props that could change but that's not the case

now apply React.memo to posts:

const PostsContainer = connect(mapPostStateToProps, mapDispatchToProps)(React.memo(Posts, areEqual));

After all that applied, UserContainer will rerender one once, and each PostContainer will rerender only once as well.

here follows link with working solution: https://codepen.io/rbuzatto/pen/BaLYmNK?editors=0010

final code:

const { useEffect } = React;
const { connect, Provider } = ReactRedux;
const { createStore, applyMiddleware, combineReducers } = Redux;
const thunk = ReduxThunk.default;

//-- REDUCERS START -- //
const userReducer = (state = [], action) => {
  if (action.type === 'fetch_users') return [...action.payload];
  return state;
};
const postReducer = (state = [], action) => {
  if (action.type === 'fetch_posts') return [...state, ...action.payload];
  return state;
};
//-- REDUCERS END -- //

//-- ACTIONS START -- //
const fetchUsers = () => async dispatch => {
  const response = await axios.get(
    'https://jsonplaceholder.typicode.com/users'
  );
  dispatch({ type: 'fetch_users', payload: response.data });
};
const fetchPosts = userId => async dispatch => {
  const response = await axios.get(
    `https://jsonplaceholder.typicode.com/users/${userId}/posts`
  );
  
  dispatch({ type: 'fetch_posts', payload: response.data });
};
//-- ACTIONS END -- //

const reducer = combineReducers({ users: userReducer, posts: postReducer });
const store = createStore(reducer, applyMiddleware(thunk));

const mapUserStateToProps = state => {
  return { users: state.users };
};

const mapPostStateToProps = (state, { userId }) => {
  return { posts: state.posts.filter(post => post.userId === userId) };
};

const mapDispatchToProps = dispatch => {
  return {
    getUsers: () => dispatch(fetchUsers()),
    getPosts: (id) => dispatch(fetchPosts(id))
  };
};

const Users = props => {
  console.log('users', props.users);

  const { getUsers } = props;
  useEffect(() => {
    getUsers();
  }, []);

  const renderUsers = () =>
    props.users.map(user => {
      return (
        <div key={user.id}>
          <div>{user.name}</div>
          <div>
            <PostsContainer userId={user.id} />
          </div>
        </div>
      );
    });
  return <div style={{backgroundColor:'green'}}>{renderUsers()}</div>;
};

const UserContainer = connect(mapUserStateToProps, mapDispatchToProps)(Users);

const Posts = props => {
  
 console.log('posts');

  const { getPosts, userId } = props;
  useEffect(() => {
    getPosts(userId);
  }, [userId]);
  
  const renderPosts = () =>
    props.posts.map(post => {
      return (
        <div>
          <div>{post.title}</div>          
        </div>
      );
    });
  
  return <div style={{backgroundColor:'yellow'}}>{renderPosts()}</div>;
};

const areEqual = (prevProps, nextProps) => {
  return JSON.stringify(prevProps.posts) === JSON.stringify(nextProps.posts)
}

const PostsContainer = connect(mapPostStateToProps, mapDispatchToProps)(React.memo(Posts, areEqual));

const App = props => {
  return (
    <div>
      <UserContainer />
    </div>
  );
};

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
Sign up to request clarification or add additional context in comments.

3 Comments

Crystal-clear explanation. There is nothing to add!
@buzatto Thank you so much for this! To be honest, I'm not very clear with React.memo, useMemo and useCallback. Do you have any good tutorials or resources for these?
you are welcome :) . I've read more articles about this topic than tutotrials. I had struggle at start also. here are some that might give you better insight about it medium.com/@jan.hesters/…. rossbulat.medium.com/how-to-memoize-in-react-3d20cbcd2b6e cheers!
0

useEffect() renders the component every time something is changed in the dependencies you provided.

Ideally, you should change your components to re-render only when something changes in props. getUser and getPost change on each render. So, it is better to change it to monitor users and posts from state.

In Users:

const { users, getUsers } = props;
useEffect(() => {
  getUsers();
}, []); -- Leaving this empty makes it load only on mount.

In Posts:

const { getPosts, userId } = props;
useEffect(() => {
   getPosts(userId);
}, [userId]);

1 Comment

I've changed the code as you mentioned but the result hasn't changed. I see the same results

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.