【问题标题】:Problem using useDispatch Hook - Warning: React has detected a change in the order of Hooks使用 useDispatch Hook 的问题 - 警告:React 检测到 Hook 的顺序发生了变化
【发布时间】:2025-11-25 22:30:01
【问题描述】:

这是我第一次在 react.-native 中使用 Redux Hooks,但是我在测试中遇到了问题,因为当我运行测试时,我收到了这个警告:

警告:React 检测到 Hooks 调用的顺序发生了变化 保修主页按钮。如果不修复,这将导致错误和错误。 欲了解更多信息,请阅读 Hooks 规则:

   Previous render            Next render
   ------------------------------------------------------
1. useState                   useContext
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

我不确定问题是我的代码还是我测试的方式,应用程序运行良好。如果我独立运行每个测试,则测试通过而没有警告,如果我运行所有三个测试,最后一个测试会抛出警告和错误。

如果你们能指出我犯错的正确方向,我将不胜感激。

WarrantiesHomeButton.js

import React, { useState, useEffect, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { WarrantiesHomeMenu } from '../WarrantiesHomeMenu';
import { userService } from '../services';
import { HomeButton } from '../components';
import { screenNames } from '../constants';
import { userActions } from '../store';
import PropTypes from 'prop-types';

const WarrantiesHomeButton = ({ navigation }) => {
  const [showWarrantiesMenu, setShowWarrantiesMenu] = useState(false);

  const { warrantiesLoginFlow, user } = useSelector(
    (state) => state.userReducer,
  );
  const dispatch = useDispatch();

  const handlePressWarranties = () => {
    if (!userService.isAuthenticated()) {
      dispatch(userActions.warrantiesLoginStart());
      navigation.navigate(screenNames.SIGN_UP);
    } else {
      setShowWarrantiesMenu(true);
    }
  };

  useEffect(() => {
    validateWarrantiesFlow();
  });

  const validateWarrantiesFlow = useCallback(() => {
    if (warrantiesLoginFlow && user.id) {
      setShowWarrantiesMenu(true);
      dispatch(userActions.warrantiesLoginStop());
    }
  }, [warrantiesLoginFlow, user.id, dispatch]);

  const handleModalPress = () => {
    setShowWarrantiesMenu(false);
    navigation.navigate(screenNames.SERVICE_CENTERS);
  };

  const closeWarrantiesHomeMenu = () => {
    setShowWarrantiesMenu(false);
  };

  return (
    <>
      <HomeButton
        testID="HomeButton"
        text="Revisiones garantía"
        icon="toolBox"
        onPress={handlePressWarranties}
      />

      <WarrantiesHomeMenu
        visible={showWarrantiesMenu}
        onPress={handleModalPress}
        navigation={navigation}
        closeModal={closeWarrantiesHomeMenu}
      />
    </>
  );
};

WarrantiesHomeButton.propTypes = {
  navigation: PropTypes.objectOf(PropTypes.any),
};

export { WarrantiesHomeButton };

WarrantiesHomeButton.test.js

import * as React from 'react';
import { Provider } from 'react-redux';
import {
  render as rtlRender,
  fireEvent,
  wait,
  act,
} from '@testing-library/react-native';
import { userService } from '../services';
import { screenNames } from '../constants';
import { WarrantiesHomeButton } from './WarrantiesHomeButton';
import { store } from '@auteco/store';

jest.mock('@auteco/components', () => require('@auteco/test').mockComponents());

jest.mock('../WarrantiesHomeMenu', () => {
  const { mockComponent } = require('@auteco/test');
  return {
    WarrantiesHomeMenu: mockComponent('WarrantiesHomeMenu'),
  };
});

describe('<WarantiesHomeButton/>', () => {
  let mockProps;
  const setState = jest.fn();
  beforeAll(() => {
    mockProps = {
      navigation: {
        navigate: jest.fn(),
        dangerouslyGetParent: jest.fn(() => ({ state: null })),
      },
    };
    jest.spyOn(userService, 'isAuthenticated').mockReturnValue(true);
  });

  function render(ui) {
    return rtlRender(<Provider store={store}>{ui}</Provider>);
  }

  it('Should Render correctly', () => {
    const { baseElement } = render(<WarrantiesHomeButton {...mockProps} />);
    expect(baseElement).toMatchSnapshot();
  });

  describe('And user is authenticated', () => {
    describe('And button is pressed', () => {
      it('Should show WarrantiesMenu by setting setShowWarrantiesMenu state to true', async () => {
        jest.spyOn(React, 'useState').mockReturnValue([false, setState]);
        const { getByTestId } = render(<WarrantiesHomeButton {...mockProps} />);
        const button = getByTestId('HomeButton');
        button.props.onPress();
        await wait();
        expect(setState).toBeCalledWith(true);
      });
    });
  });

  describe('And user is not authenticated', () => {
    describe('And button is pressed', () => {
      it('Should navigate to signUpScreen', async () => {
        jest.spyOn(userService, 'isAuthenticated').mockReturnValue(false);
        jest.spyOn(React, 'useState').mockReturnValue([false, setState]);
        const { getByTestId } = render(<WarrantiesHomeButton {...mockProps} />);
        const button = getByTestId('HomeButton');
        act(() => {
          button.props.onPress();
        });
        expect(mockProps.navigation.navigate).toBeCalledWith(
          screenNames.SIGN_UP,
        );
      });
    });
  });
});

控制台出错

  console.error node_modules/react-native/Libraries/YellowBox/YellowBox.js:63
    Warning: React has detected a change in the order of Hooks called by WarrantiesHomeButton. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks

       Previous render            Next render
       ------------------------------------------------------
    1. useState                   useContext
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

        in WarrantiesHomeButton
        in Provider
        in View (created by View)
        in View (created by AppContainer)
        in View (created by View)
        in View (created by AppContainer)
        in AppContainer (at src/index.js:26)

  console.error node_modules/react-native/Libraries/YellowBox/YellowBox.js:63
    The above error occurred in the <WarrantiesHomeButton> component:
        in WarrantiesHomeButton
        in Provider
        in View (created by View)
        in View (created by AppContainer)
        in View (created by View)
        in View (created by AppContainer)
        in AppContainer (at src/index.js:26)

    Consider adding an error boundary to your tree to customize error handling behavior.


 FAIL  src/components/home/WarrantiesHomeButton/WarrantiesHomeButton.test.js
  <WarantiesHomeButton/>
    ✓ Should Render correctly (55ms)
    And user is authenticated
      And button is pressed
        ✓ Should show WarrantiesMenu by setting setShowWarrantiesMenu state to true (12ms)
    And user is not authenticated
      And button is pressed
        ✕ Should navigate to signUpScreen (77ms)

  ● <WarantiesHomeButton/> › And user is not authenticated › And button is pressed › Should navigate to signUpScreen

    TypeError: Cannot read property 'length' of undefined

      11 |   const [showWarrantiesMenu, setShowWarrantiesMenu] = useState(false);
      12 | 
    > 13 |   const { warrantiesLoginFlow, user } = useSelector(
         |                                         ^
      14 |     (state) => state.userReducer,
      15 |   );
      16 |   const dispatch = useDispatch();

      at areHookInputsEqual (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:5703:38)
      at updateMemo (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:6336:11)
      at Object.useMemo (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:6703:16)
      at useMemo (node_modules/react/cjs/react.development.js:1592:21)
      at useSelectorWithStoreAndSubscription (node_modules/react-redux/lib/hooks/useSelector.js:31:41)
      at useSelector (node_modules/react-redux/lib/hooks/useSelector.js:117:12)
      at WarrantiesHomeButton (src/components/home/WarrantiesHomeButton/WarrantiesHomeButton.js:13:41)
      at renderWithHooks (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:5762:18)
      at updateFunctionComponent (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:7579:20)
      at beginWork$1 (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:9152:16)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 2 passed, 3 total
Snapshots:   1 passed, 1 total
Time:        4.846s, estimated 5s
Ran all test suites matching /WarrantiesHomeButton/i.

Watch Usage: Press w to show more.

【问题讨论】:

    标签: reactjs react-native redux react-hooks


    【解决方案1】:
       Previous render            Next render
       ------------------------------------------------------
    1. useState                   useState
    2. useState                   useState
    3. useState                   useState
    4. useState                   useState
    5. useContext                 useEffect
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    

    如您在上面观察到的,我遇到了类似的问题。我最终通过将it 块中每个组件的创建/渲染包装在act 中来解决它。似乎useEffect 对来自测试的组件的异步操作相互冲突。我解决了如下所示:

    let component: any;
    await act(async () => {
      component = create(
        <MockedProvider>
          <SelectAccountScreen {...props} />
        </MockedProvider>,
      );
    });
    

    PS:我使用的是react-test-renderer(如果你想知道create来自哪里)

    【讨论】: