You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Redux中createOrder reducer为何触发两次?

点击Place Order按钮时createOrder被dispatch两次的问题

当点击placeOrderScreen.js中的Place Order按钮时,placeOrderHandler执行并dispatch createOrder——按逻辑应该仅触发一次,但实际执行了两次:一次在useEffect执行前,一次在useEffect执行后。

useEffect执行前

useEffect执行前

useEffect执行后

useEffect执行后

需求:点击Place Order按钮时createOrder仅执行一次。


相关代码

placeOrderScreen.js

import React, { useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';

import {
  Button,
  Row,
  Col,
  ListGroup,
  Image,
  Card,
  ListGroupItem,
} from 'react-bootstrap';
import { useDispatch, useSelector } from 'react-redux';
import Message from '../components/Message';
import CheckoutSteps from '../components/CheckoutSteps';
import {
  createOrder,
  ORDER_CREATE_RESET,
} from '../features/OrderFeature/orderCreateSlice';
import { USER_DETAILS_RESET } from '../features/UserFeature/userDetailsSlice';
const PlaceOrderScreen = () => {
  const navigate = useNavigate();
  const dispatch = useDispatch();
  const cart = useSelector((store) => store.cart);
  //计算价格
  const addDecimals = (num) => {
    return (Math.round(num * 100) / 100).toFixed(2);
  };
  //为了修改Cart对象,创建了一个本地副本……因为需要添加一些额外内容(价格)
  const cartObject = { ...cart };
  cartObject.itemsPrice = addDecimals(
    cartObject.cartItems.reduce((acc, item) => acc + item.price * item.qty, 0)
  );
  cartObject.shippingPrice = addDecimals(cartObject.itemsPrice < 100 ? 0 : 100);
  cartObject.taxPrice = addDecimals(
    Number((0.15 * cartObject.itemsPrice).toFixed(2))
  );
  cartObject.totalPrice = (
    Number(cartObject.itemsPrice) +
    Number(cartObject.shippingPrice) +
    Number(cartObject.taxPrice)
  ).toFixed(2);

  const orderCreate = useSelector((store) => store.orderCreate);
  const { order, success, error } = orderCreate;

  useEffect(() => {
    //如果一切正常(在orderSlice中)且获取到数据,则……
    if (success) {
      //如果获取到订单数据,则提取其中的_id属性
      navigate(`/order/${order._id}`);
      dispatch(ORDER_CREATE_RESET());
      dispatch(USER_DETAILS_RESET());
    }
    // 消除order._id依赖警告
    // eslint-disable-next-line
  }, [navigate, success]);
  const placeOrderHandler = () => {
    dispatch(
      createOrder({
        orderItems: cart.cartItems,
        shippingAddress: cart.shippingAddress,
        paymentMethod: cart.paymentMethod,
        itemsPrice: cartObject.itemsPrice, //取自复制Cart对象得到的新对象
        shippingPrice: cartObject.shippingPrice,
        taxPrice: cartObject.taxPrice,
        totalPrice: cartObject.totalPrice,
      })
    );
  };

  return (
    <>
      <CheckoutSteps step1 step2 step3 step4 />
      <Row>
        <Col md={8}>
          <ListGroup variant='flush'>
            <ListGroup.Item>
              <h2>Shipping</h2>
              <p>
                <strong>Address:</strong>
                {cart.shippingAddress.address},{cart.shippingAddress.city},
                {cart.shippingAddress.postalCode},{cart.shippingAddress.country}
                ,
              </p>
            </ListGroup.Item>
            <ListGroup.Item>
              <h2>Payment Method</h2>
              <strong>Method:</strong>
              {cart.paymentMethod}
            </ListGroup.Item>

            <ListGroup.Item>
              <h2>Order Items</h2>
              {cart.cartItems.length === 0 ? (
                <Message>Your Cart is empty</Message>
              ) : (
                <ListGroup variant='flush'>
                  {cart.cartItems.map((item, index) => {
                    return (
                      <ListGroupItem key={index}>
                        <Row>
                          <Col md={1}>
                            <Image
                              src={item.image}
                              alt={item.name}
                              fluid
                              rounded
                            />
                          </Col>
                          <Col>
                            <Link to={`/product/${item.product}`}>
                              {item.name}
                            </Link>
                          </Col>

                          <Col md={4}>
                            {item.qty} x ${item.price}=${item.qty * item.price}
                          </Col>
                        </Row>
                      </ListGroupItem>
                    );
                  })}
                </ListGroup>
              )}
            </ListGroup.Item>
          </ListGroup>
        </Col>
        <Col md={4}>
          <Card>
            <ListGroup variant='flush'>
              <ListGroup.Item>
                <h2>Order Summary</h2>
              </ListGroup.Item>
              <ListGroup.Item>
                <Row>
                  <Col>Items</Col>
                  <Col>${cartObject.itemsPrice}</Col>
                </Row>
              </ListGroup.Item>
              <ListGroup.Item>
                <Row>
                  <Col>Shipping</Col>
                  <Col>${cartObject.shippingPrice}</Col>
                </Row>
              </ListGroup.Item>
              <ListGroup.Item>
                <Row>
                  <Col>Tax</Col>
                  <Col>${cartObject.taxPrice}</Col>
                </Row>
              </ListGroup.Item>
              <ListGroup.Item>
                <Row>
                  <Col>Total</Col>
                  <Col>${cartObject.totalPrice}</Col>
                </Row>
              </ListGroup.Item>
              <ListGroupItem>
                {error && <Message variant='danger'>{error}</Message>}
              </ListGroupItem>
              <ListGroup.Item>
                <Button
                  type='button'
                  className='btn-block'
                  disabled={cart.cartItems === 0}
                  onClick={placeOrderHandler}
                >
                  Place Order
                </Button>
              </ListGroup.Item>
            </ListGroup>
          </Card>
        </Col>
      </Row>
    </>
  );
};

export default PlaceOrderScreen;

orderCreateSlice.js

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';

const initialState = {};
export const createOrder = createAsyncThunk(
  'createOrder',
  async (order, thunkAPI) => {
    try {
      const {
        //从getState()中获取userLogin,并赋值给userInfo变量
        userLogin: { userInfo },
      } = thunkAPI.getState();

      const config = {
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${userInfo.token}`,
        },
      };
      const { data } = await axios.post(`/api/orders`, order, config);
      return data;
    } catch (error) {
      const newError =
        error.response && error.response.data.message
          ? error.response.data.message
          : error.message;
      //这会作为payload进入rejected分支……仅返回error时会执行fulfilled动作
      return thunkAPI.rejectWithValue(newError);
    }
  }
);

const orderCreateSlice = createSlice({
  name: 'orderCreate',
  initialState,
  reducers: {
    ORDER_CREATE_RESET: () => {
      return {};
    },
  },
  extraReducers: {
    //extra reducers仅用于异步操作……通常使用普通reducer
    [createOrder.pending]: (state) => {
      return {
        loading: true,
      };
    },

    [createOrder.fulfilled]: (state, action) => {
      return {
        loading: false,
        success: true,
        order: action.payload,
      };
    },

    [createOrder.rejected]: (state, action) => {
      return {
        loading: false,
        error: action.payload,
      };
    },
  },
});
export const { ORDER_CREATE_RESET } = orderCreateSlice.actions;
export default orderCreateSlice.reducer;

store.js

import { configureStore } from '@reduxjs/toolkit';
import productListReducer from './features/productListFeature/productListSlice';
import productDetailsReducer from './features/productListFeature/productDetailSlice';
import CartReducer from './features/addToCart/cartSlice';
import userLoginReducer from './features/UserFeature/loginUserSlice';
import userRegisterReducer from './features/UserFeature/registerUserSlice';
import userDetailsReducer from './features/UserFeature/userDetailsSlice';
import userUpdateProfileReducer from './features/UserFeature/updateProfileSlice';
import orderCreateReducer from './features/OrderFeature/orderCreateSlice';
import orderDetailsReducer from './features/OrderFeature/orderDetailsSlice';
//这里我们从localStorage中获取存储在cartSlice的数据……因为在cartSlice中用stringify处理过数据,所以要运行JSON.parse
const cartItemsFromStorage = localStorage.getItem('cartItems')
  ? JSON.parse(localStorage.getItem('cartItems'))
  : [];

const userInfoFromStorage = localStorage.getItem('userInfo')
  ? JSON.parse(localStorage.getItem('userInfo'))
  : null; //如果用户信息不可用,则返回null
const shippingAddressFromStorage = localStorage.getItem('shippingAddress')
  ? JSON.parse(localStorage.getItem('shippingAddress'))
  : {};

const initialState = {
  cart: {
    cartItems: cartItemsFromStorage,
    shippingAddress: shippingAddressFromStorage,
  },
  userLogin: {
    userInfo: userInfoFromStorage,
  },
};

const store = configureStore({
  reducer: {
    productList: productListReducer,
    productDetails: productDetailsReducer,
    cart: CartReducer,
    userLogin: userLoginReducer,
    userRegister: userRegisterReducer,
    userDetails: userDetailsReducer,
    userUpdateProfile: userUpdateProfileReducer,
    orderCreate: orderCreateReducer,
    orderDetails: orderDetailsReducer,
  },
  preloadedState: initialState, //用于本地存储
});
export default store;

问题原因与解决方法

核心原因

最常见的触发场景是React严格模式(Strict Mode):在开发环境下,React Strict Mode会刻意重复调用组件渲染函数、事件处理函数及部分生命周期方法,用于检测代码中的潜在副作用问题,这会直接导致placeOrderHandler被调用两次,进而dispatch两次createOrder。

解决方案

  1. 开发环境无需特殊处理:Strict Mode仅在开发环境生效,生产环境会自动恢复单次触发逻辑,所以如果只是开发阶段出现该问题,无需修改代码。若要在开发环境验证单次效果,可临时移除index.js中的<React.StrictMode>包裹,但不建议长期这么做——Strict Mode能帮你提前发现代码隐患。

  2. 添加防重复提交逻辑:利用Redux状态中的loading标记禁用按钮,避免请求未完成时重复点击:

// 修改PlaceOrderScreen中的按钮属性
<Button
  type='button'
  className='btn-block'
  disabled={cart.cartItems.length === 0 || orderCreate.loading}
  onClick={placeOrderHandler}
>
  {orderCreate.loading ? 'Placing Order...' : 'Place Order'}
</Button>
  1. 优化事件处理函数:用useCallback包裹placeOrderHandler,避免组件每次渲染时重新创建函数,虽不直接解决Strict Mode的重复调用,但能提升性能并规避潜在的重复触发场景:
import React, { useEffect, useCallback } from 'react';

// ... 其他代码

const placeOrderHandler = useCallback(() => {
  dispatch(
    createOrder({
      orderItems: cart.cartItems,
      shippingAddress: cart.shippingAddress,
      paymentMethod: cart.paymentMethod,
      itemsPrice: cartObject.itemsPrice,
      shippingPrice: cartObject.shippingPrice,
      taxPrice: cartObject.taxPrice,
      totalPrice: cartObject.totalPrice,
    })
  );
}, [dispatch, cart, cartObject]);

内容的提问来源于stack exchange,提问作者Usama BG

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 10:10:29