Redux: mapStateToProps is rendering twice

Viewed 129

I have started learning Redux recently, and something is bugging me.

import React, { useEffect } from "react";
import { connect, useDispatch } from "react-redux";
import Modal from "../Modal/Modal";
import history from "../../utils/history";
import { fetchPost } from "../../actions";

const PostDelete = ({ match, post }) => {
  const postId = match.params?.id;
  const dispatch = useDispatch();
  useEffect(() => {
    dispatch(fetchPost(postId));
  }, [dispatch]);
  return (
    <Modal
    />
  );
};

const mapStateToProps = (state, { match }) => {
  console.log("MSTP", state.posts[match.params?.id]) // <== CONSOLED TWICE !!
  return { post: state.posts[match.params?.id] };
};

export default connect(mapStateToProps, {})(PostDelete);

When I navigate to this using react-router, as per my understanding:

  1. MSTP should be called first(which fetches the post from the store)
  2. Then useEffect() fetches the post(just in case user directly opens this page)
  3. It dispatches the action which changes the state
  4. This re-renders the MSTP again

Is there a way to get around this? Is this a bad approach or am I missing something here?

1 Answers

Explanation

First of all I'd like to say your understanding of what's happening is correct. From the official react-redux documentation it describes that mapStateToProps is called every time the store is updated.

This is ok if you have a fairly simple mapStateToProps object to compute, but can cause performance degradations if you're doing something more intensive. For intensive cases I'd recommend using a memoized selector, which will just return the previously calculated mapStateToProps value, without doing any new computations, if no relevant changes were made to the store. A good library for achieving this is reselect.

Even with a memoized selector, your console.log('MSTP') statement will be printed, but the underlying computation will be quicker.

Code Example

Consider the following example.

  1. Component is rendered for the first time
  2. useEffect fetches the post and updates the store at state.posts (relevant to this component)
  3. Some other component updates the redux state, at an irrelevant part to this component, e.g. state.comments

Here's the code and console output BEFORE using a memoized selector

const intensivePostsFormatting = (state) => {
   console.log('Formatting Posts');
   // do some stuff with state.posts
   return formattedPosts;
}

const mapStateToProps = ({ match, state }) => {
  console.log('MSTP');
  return {
    posts: intensivePostsFormatting(state)
   }
}

// Console Output:
// MSTP
// Formatting Posts
// MSTP
// Formatting Posts
// MSTP
// Formatting Posts

Here's the code and output AFTER using a memoized selector

import { createSelector } from 'reselect';

const intensivePostsFormatting = (posts) => {
   console.log('Formatting Posts');
   // do some stuff with posts
   return formattedPosts;
}

const postsSelector = createSelector(
   state => state.posts,
   posts => intensivePostsFormatting(posts)
)

const mapStateToProps = ({match, state }) => {
  console.log('MSTP');
  return {
    posts: postsSelector(state)
   }
}

// Console Output:
// MSTP
// Formatting Posts
// MSTP
// Formatting Posts
// MSTP

Note that the difference between the before and after, is that "Formatting Posts" is logged 3 times in the "before" example and 2 times in the "after" example. This is because using a memoized selector allowed us to skip computing the formatted posts when a change to something other than state.posts was made.

Related