Skip to content
New issue

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

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

Already on GitHub? # to your account

React & redux state mismatch, when using dispatch from useEffect #2188

Open
1 task done
nikitar opened this issue Jul 3, 2024 · 2 comments
Open
1 task done

React & redux state mismatch, when using dispatch from useEffect #2188

nikitar opened this issue Jul 3, 2024 · 2 comments

Comments

@nikitar
Copy link

nikitar commented Jul 3, 2024

What version of React, ReactDOM/React Native, Redux, and React Redux are you using?

  • React: 18.3.1
  • ReactDOM: 18.3.1
  • Redux: 4.2.1
  • React Redux: 8.1.2

What is the current behavior?

Made a simple 'counter' component that increments a counter when you click a button. It has more indirection than necessary, but it's a simplified version of a bug in some older code I'm looking at. Found it when trying to upgrade react-redux from 7 to 8. (Works fine in 7)

The basic issue is that setPendingIncrement(false) updates local state, then dispatch(counterActions.setValue(count + 1)) updates Redux state, however on the next render the local state update is not visible (yet) and Redux state change is already visible.

Now, I've read about stale props and zombie children, so I'm aware that the component will eventually re-render with both states in sync. That's fine, since the render function itself has no side-effects. However, I didn't expect the discrepancy to extend to useEffect. In useEffect, we do have side-effects, e.g. we could send a request to the server.

import {useEffect, useState} from 'react'
import './App.css'
import {useSelector, useDispatch} from 'react-redux';
import {counterActions} from './slices/counterSlice';


function App() {
    const count = useSelector((state) => state.counter.value);
    const dispatch = useDispatch();
    const [pendingIncrement, setPendingIncrement] = useState(false);

    console.log(`RENDER   ${count}   ${pendingIncrement}`);
    const onClick = () => {
        setPendingIncrement(true);
    }

    useEffect(() => {
        if (pendingIncrement) {
            setPendingIncrement(false);
            dispatch(counterActions.setValue(count + 1));
        }
    }, [pendingIncrement, setPendingIncrement, count, dispatch]);

    return (
        <button onClick={onClick}>
            count is {count}
        </button>
    )
}

export default App;

Complete example: https://snack.expo.dev/-JCou0gvPMHLWkAO5InqU
(snack.expo.dev doesn't let you print to console though, so it's hard to debug there)

What is the expected behavior?

useEffect is called with React state and Redux state in sync. Or, if this pattern violates some principles of Redux, I'd love to understand the details. I.e. what exactly we can and cannot do.

This code works fine with react-redux 7, so it's at least a regression.

Which browser and OS are affected by this issue?

macos 14.5 / Chrome 126.0.6478.127

Did this work in previous versions of React Redux?

  • Yes
@markerikson
Copy link
Contributor

This is probably the same issue as #1912 and #2086 - it's a limitation of useSyncExternalStore.

@EskiMojo14
Copy link
Collaborator

in general, any next state that's calculated based on a previous state belongs in the reducers. that's the only way to guarantee that it updates correctly, similar to using a callback with useState.

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

No branches or pull requests

3 participants