Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useSelector returns stale value in parent component if child component dispatches action updating that value during render #1599

Closed
tianenli opened this issue Jul 2, 2020 · 3 comments

Comments

@tianenli
Copy link

tianenli commented Jul 2, 2020

What is the current behavior?

Demo: https://codesandbox.io/s/race-condition-x1h1d

In this example, our parent component consumes a value from the store, and a child component on render calls an action to update that value multiple times. We see the child component receives the updated value, while the parent component does not after the first update. The parent component's useSelector instance doesn't invoke the selector function and just returns the cached value.

Wrapping the child component's action dispatch in a useEffect solves the issue.
Defining the selector in-line so that each render passes a different function reference, forcing useSelector to call the selector again also works around the issue.

The real-life use case for this pattern is that we have a redirect caused by rendering a react-router redirect component, and this causes an update to the redux store which is storing the current location thru connected-react-router, and the parent component to this consumes location from the store.

import React, { useCallback, useEffect } from "react";
import { incrementAction } from "./actions";
import { useSelector, useDispatch } from "react-redux";

const countersSelector = state => {
  console.log("RUNNING SELECTOR", state.counters);
  return state.counters;
};

function Child(props) {
  const dispatch = useDispatch();
  const increment = useCallback(() => dispatch(incrementAction()), [dispatch]);

  console.log("Child CALLING USESELECTOR");
  const counters = useSelector(countersSelector);
  console.log(counters);

  // wrapping this in a useEffect gets around the issue
  if (counters.counter < 3) {
    console.log("CHILD IS INCREMENTING");
    increment();
  }

  return <div>Child: {counters.counter}</div>;
}

function App(props) {
  console.log("App CALLING USESELECTOR");
  const counters = useSelector(countersSelector);
  console.log(counters);

  return (
    <div className="App">
      <div>App: {counters.counter}</div>
      <Child />
    </div>
  );
}

export default App;

What is the expected behavior?

We would expect the parent component's useSelector instance to return the same value from the store as the child component's

Which versions of React, ReactDOM/React Native, Redux, and React Redux are you using? Which browser and OS are affected by this issue? Did this work in previous versions of React Redux?

The versions used in the code sandbox are:
react: 16.13.1
react-dom: 16.8.3
react-redux: 7.1.3
redux: 4.0.5

@tianenli tianenli changed the title Possible race condition if child component dispatches action during render useSelector returns stale value in parent component if child component dispatches action updating that value during render Jul 2, 2020
@timdorr
Copy link
Member

timdorr commented Jul 2, 2020

This is expected behavior because you are not wrapping a side effect during render in useEffect. This is just how React works and would happen the same if you used a global variable.

@timdorr timdorr closed this as completed Jul 2, 2020
@tianenli
Copy link
Author

tianenli commented Jul 2, 2020

I've updated the example to replace the child comment's side effects on render with a class component with side effects on componentDidMount and componentDidUpdate, and it yields similar issues.

https://codesandbox.io/s/race-condition-x1h1d

import React from "react";
import { incrementAction } from "./actions";
import { connect, useSelector } from "react-redux";

const countersSelector = state => {
  console.log("RUNNING SELECTOR", state.counters);
  return state.counters;
};

class ChildBase extends React.Component {
  componentDidMount() {
    if (this.props.counter < 3) {
      this.props.increment();
    }
  }

  componentDidUpdate() {
    if (this.props.counter < 3) {
      this.props.increment();
    }
  }

  render() {
    return <div>Child: {this.props.counter}</div>;
  }
}

const mapStateToProps = state => ({
  counter: state.counters.counter
});

const mapDispatchToProps = dispatch => ({
  increment: () => dispatch(incrementAction())
});

const Child = connect(
  mapStateToProps,
  mapDispatchToProps
)(ChildBase);

function App(props) {
  console.log("App CALLING USESELECTOR");
  const counters = useSelector(countersSelector);
  console.log(counters);

  return (
    <div className="App">
      <div>App: {counters.counter}</div>
      <Child />
    </div>
  );
}

export default App;

@dai-shi
Copy link
Contributor

dai-shi commented Jul 2, 2020

The last one should be fixed by #1536. (I didn't notice that it's not only with useCallback but it applies to a stable selector defined outside render.)

You could also use useEffect to avoid the issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants