bglownia
bglownia

Reputation: 41

mapStateToProps of child component invoked before render of parent component

Element is using useSelector hook, it's getting value from store and basing on the value it conditionally renders . is connected to store using connect. Issue is that mapStateToProps of Child component is executed before render of Parent component.

This causes that child mapStateToProps is called even when parent will not render it with new state. I expected top to bottom update of subscribed components, including calling mapStateToProps.

I created minimal implementation to recreate this scenario https://codesandbox.io/s/red-snow-g7i0n?file=/src/index.js

const getValue = (state) => state.value;
const mapStateToPros = (state) => {
  console.log("child map state to props:", state.value);
  return { value: state.value };
};

class Child extends Component {
  render() {
    console.log("child render:", this.props.value);
    return <span>{this.props.value}</span>;
  }
}

const ConnectedChild = connect(mapStateToPros)(Child);

function Parent() {
  const dispatch = useDispatch();
  const value = useSelector(getValue);
  console.log("parent render:", value);
  return (
    <>
      <button onClick={() => dispatch({ type: "INCREASE_VALUE" })}>
        action
      </button>
      {value < 3 ? <ConnectedChild /> : null}
    </>
  );
}

on console you will see

parent render: 1
child map state to props: 1
child render: 1

child map state to props: 2
parent render: 2
child render: 2

child map state to props: 3
parent render: 3

parent render: 4

Is this expected behavior, or maybe bug or corner case?

Upvotes: 2

Views: 635

Answers (2)

markerikson
markerikson

Reputation: 67469

I commented on this over in Reactiflux, but I'll answer here for visibility.

There are implementation differences between connect and useSelector. connect is implemented to cascade store updates in a top-down sequence, so that connected components higher in the component tree are guaranteed to have completely finished rendering before nested connected children have their mapStates run.

However, if you don't have nested uses of connect, then all components are going to be subscribing to the store directly. That means that all store subscriptions will run immediately and sequentially after a dispatch, regardless of where they are in the component tree, and thus before any component has a chance to re-render. So, this sounds like entirely expected behavior given the implementation.

I am curious why you're mixing useSelector and connect in the same component tree like this :)

For more details, see:

Upvotes: 4

phry
phry

Reputation: 44086

mapStateToProps will, just like every useSelector selector, be called on every state change because otherwise it could not make the decision if a component needs to be re-rendered. This is expected behaviour. As you see it does not directly cause any rerender of the connected component.

The order pretty much does not matter: If incoming props are equal and state is equal, the child does not have to rerender. If incoming props from a parent change, it will have to rerender no matter what (and then also take your new state & props into account).

Upvotes: 1

Related Questions