React Redux中createOrder reducer为何触发两次?
点击Place Order按钮时createOrder被dispatch两次的问题
当点击placeOrderScreen.js中的Place Order按钮时,placeOrderHandler执行并dispatch createOrder——按逻辑应该仅触发一次,但实际执行了两次:一次在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。
解决方案
开发环境无需特殊处理:Strict Mode仅在开发环境生效,生产环境会自动恢复单次触发逻辑,所以如果只是开发阶段出现该问题,无需修改代码。若要在开发环境验证单次效果,可临时移除
index.js中的<React.StrictMode>包裹,但不建议长期这么做——Strict Mode能帮你提前发现代码隐患。添加防重复提交逻辑:利用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>
- 优化事件处理函数:用
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
相关产品推荐
相关产品推荐

