reduxjs / redux

A JS library for predictable global state management
https://redux.js.org
MIT License
60.93k stars 15.27k forks source link

Maximum update depth exceeded error when using Redux with useEffect #4738

Closed weirenxue closed 2 months ago

weirenxue commented 2 months ago

Prior Issues

I did not find any existing issues or PRs that relate to this problem.

What is the current behavior?

I am encountering a "Maximum update depth exceeded" error in my React application when using Redux with useEffect. This issue occurs intermittently when clicking the "App1 click me" button. However, clicking the "App2 click me" button does not cause the error.

Steps to Reproduce

  1. Create a new React application with Redux and Redux Toolkit.
  2. Add the following code to the application:

    import { useState, useEffect } from 'react';
    import { configureStore, createSlice } from '@reduxjs/toolkit';
    import { Provider, useSelector, useDispatch } from 'react-redux';
    
    const someSlice = createSlice({
      name: 'some',
      initialState: {
        something: {},
      },
      reducers: {
        updateSomething: (state) => {
          state.something = {};
        },
      },
    });
    
    const { updateSomething } = someSlice.actions;
    
    const store = configureStore({
      reducer: {
        some: someSlice.reducer,
      },
    });
    
    function App1() {
      const [foo, setFoo] = useState({});
      const [bar, setBar] = useState(0);
    
      const something = useSelector((state) => state.some.something);
    
      const dispatch = useDispatch();
    
      useEffect(() => {
        Promise.all([]).then(() => {
          setFoo((t) => ({ ...t }));
        });
      }, [bar]);
    
      useEffect(() => {
        console.log('App1 foo changed');
      }, [foo]);
    
      useEffect(() => {
        dispatch(updateSomething());
      }, [dispatch, foo]);
    
      useEffect(() => {
        setInterval(() => {
          setBar((t) => t + 1);
        }, 1);
      }, []);
    
      return (
        <button
          type="button"
          onClick={() => {
            setBar((t) => t + 1);
          }}
        >
          (App1)click me
        </button>
      );
    }
    
    function App2Component({ something, setSomething }) {
      const [foo, setFoo] = useState({});
      const [bar, setBar] = useState(0);
    
      useEffect(() => {
        Promise.all([]).then(() => {
          setFoo((t) => ({ ...t }));
        });
      }, [bar]);
    
      useEffect(() => {
        console.log('App2 foo changed');
      }, [foo]);
    
      useEffect(() => {
        setSomething({});
      }, [setSomething, foo]);
    
      useEffect(() => {
        setInterval(() => {
          setBar((t) => t + 1);
        }, 1);
      }, []);
    
      return (
        <button
          type="button"
          onClick={() => {
            setBar((t) => t + 1);
          }}
        >
          (App2)click me
        </button>
      );
    }
    
    function App2() {
      const [something, setSomething] = useState({});
      return <App2Component something={something} setSomething={setSomething} />;
    }
    
    function App() {
      return (
        <>
          <Provider store={store}>
            <App1 />
          </Provider>
          <App2 />
        </>
      );
    }
    
    export default App;
  3. Run the application.
  4. Click the "App1 click me" button multiple times and observe the console for the "Maximum update depth exceeded" error. Screenshot 2024-09-11 at 8 36 38 AM
  5. Click the "App2 click me" button multiple times and observe that the error does not occur.

You can also reproduce the issue using this CodeSandbox link: https://codesandbox.io/p/sandbox/empty-forest-jz8whm

What is the expected behavior?

Redux should not cause a "Maximum update depth exceeded" error when using useEffect in this manner.

Environment Details

Redux version: ^9.1.2 Redux Toolkit version: ^2.2.7 React version: ^18.3.1 React DOM version: ^18.3.1 Browser: Edge OS: macOS 14.5

markerikson commented 2 months ago

This isn't a Redux core issue, and it presumably isn't an issue with Redux or React-Redux at all. Setting state in a useEffect should be only be done rarely and with caution:

In general this looks like a very complicated and chained set of useEffects, I don't know what the actual purpose of this is, and it looks like it's contrived to cause a recurring series of React re-renders. This doesn't have anything to do with Redux.