I've got this issue which I do not understand. I am getting an invariant error that says a state mutation has occurred, but at a very awkward place.
Basically I've got this page:
import React, { useState, useEffect } from "react";
import {
Link,
useLocation,
useNavigate,
useSearchParams,
} from "react-router-dom";
import { Form, Button, Row, Col, Table } from "react-bootstrap";
import { LinkContainer } from "react-router-bootstrap";
import { useDispatch, useSelector } from "react-redux";
import Loader from "../components/Loader";
import Message from "../components/Message";
import FormContainer from "../components/FormContainer";
import { register } from "../actions/userActions";
import { getUserDetails, updateUserProfile } from "../actions/userActions";
import { USER_UPDATE_PROFILE_RESET } from "../constants/userConstants";
import { listMyOrders } from "../actions/orderActions";
function ProfileScreen() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [message, setMessage] = useState("");
const dispatch = useDispatch();
const location = useLocation();
const userDetails = useSelector((state) => state.userDetails);
const navigate = useNavigate();
const { error, loading, user } = userDetails;
const userLogin = useSelector((state) => state.userLogin);
const { userInfo } = userLogin;
const userUpdateProfile = useSelector((state) => state.userUpdateProfile);
const { success } = userUpdateProfile;
const orderListMy = useSelector((state) => state.orderListMy);
const { loading: loadingOrders, error: errorOrders, orders } = orderListMy;
useEffect(() => {
if (!userInfo) {
navigate("/login");
} else {
if (!user || !user.name || success) {
dispatch({ type: USER_UPDATE_PROFILE_RESET });
dispatch(getUserDetails("profile"));
dispatch(listMyOrders());
} else {
setName(user.name);
setEmail(user.email);
}
}
}, [dispatch, location, userInfo, user, success]);
const submitHandler = (e) => {
e.preventDefault();
if (password != confirmPassword) {
setMessage("Passwords do not match!");
} else {
dispatch(
updateUserProfile({
id: user._id,
name: name,
email: email,
password: password,
})
);
}
};
return (
<Row>
<Col md={3}>
<h2>User Profile</h2>
{message && <Message variant="danger">{message}</Message>}
{error && <Message variant="danger">{error}</Message>}
{loading && <Loader></Loader>}
<Form onSubmit={submitHandler}>
<Form.Group controlId="name">
<Form.Label>Name</Form.Label>
<Form.Control
required
type="name"
placeholder="Enter Name"
value={name}
onChange={(e) => setName(e.target.value)}
></Form.Control>
</Form.Group>
<Form.Group controlId="email">
<Form.Label>Email Address</Form.Label>
<Form.Control
required
type="email"
placeholder="Enter Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
></Form.Control>
</Form.Group>
<Form.Group controlId="password">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
placeholder="Enter Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
></Form.Control>
</Form.Group>
<Form.Group controlId="passwordConfirm">
<Form.Label>Confirm Password</Form.Label>
<Form.Control
type="password"
placeholder="Confirm Password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
></Form.Control>
</Form.Group>
<Button type="submit" variant="primary">
Update
</Button>
</Form>
</Col>
<Col md={9}>
<h2>My Orders</h2>
{loadingOrders ? (
<Loader />
) : errorOrders ? (
<Message variant="danger">{errorOrders}</Message>
) : (
<Table striped responsive className="table-sm">
<thead>
<tr>
<th>ID</th>
<th>Date</th>
<th>Total</th>
<th>Paid</th>
<th>Delivered</th>
<th></th>
</tr>
</thead>
<tbody>
{orders.map((order) => (
<tr key={order._id}>
<td>{order._id}</td>
<td>{order.createdAt.substring(0, 10)}</td>
<td>${order.totalPrice}</td>
<td>
{order.isPaid ? (
order.paidAt.substring(0, 10)
) : (
<i className="fas fa-times" style={{ color: "red" }}></i>
)}
</td>
<td>
<LinkContainer to={`/order/${order._id}`}>
<Button className="btn-sm">Details</Button>
</LinkContainer>
</td>
</tr>
))}
</tbody>
</Table>
)}
</Col>
</Row>
);
}
export default ProfileScreen;
and in this page whenever you click on the Details button on some of the orders you get redirected to this page:
import React, { useState, useEffect } from "react";
import { Button, Row, Col, ListGroup, Image, Card } from "react-bootstrap";
import { Link, useNavigate, useParams } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { PayPalButton } from "react-paypal-button-v2";
import Message from "../components/Message";
import Loader from "../components/Loader";
import CheckoutSteps from "../components/CheckoutSteps";
import {
createOrder,
getOrderDetails,
payOrder,
} from "../actions/orderActions";
import {
ORDER_CREATE_RESET,
ORDER_PAY_RESET,
} from "../constants/orderConstants";
function OrderScreen() {
const orderDetails = useSelector((state) => state.orderDetails);
const { order, error, loading } = orderDetails;
const orderPay = useSelector((state) => state.orderPay);
const { loading: loadingPay, success: successPay } = orderPay;
const dispatch = useDispatch();
const history = useNavigate();
const orderId = useParams();
const takenId = orderId.id;
const [sdkReady, setSdkReady] = useState(false);
const cart = useSelector((state) => state.cart);
let copyCart = Object.assign({}, cart);
if (!loading && !error) {
order.itemsPrice = order.orderItems
.reduce((acc, item) => acc + item.price * item.qty, 0)
.toFixed(2);
}
const addPayPalScript = () => {
const script = document.createElement("script");
script.type = "text/javascript";
script.src =
"https://www.paypal.com/sdk/js?client-id=ATMiiNcpLBRgha0mQCRpS5GJRC9YrATzPxyLFvWGoVsCvoxg46E0UGielBSPptskQWiqk8mrp2eQgpnC";
script.async = true;
script.onload = () => {
setSdkReady(true);
};
document.body.appendChild(script);
};
useEffect(() => {
if (!order || successPay || order._id !== Number(takenId)) {
dispatch({ type: ORDER_PAY_RESET });
dispatch(getOrderDetails(takenId));
} else if (!order.isPaid) {
if (!window.paypal) {
addPayPalScript();
} else {
setSdkReady(true);
}
}
}, [dispatch, order, orderId, successPay]);
const successPaymentHandler = (paymentResult) => {
dispatch(payOrder(takenId, paymentResult));
};
return loading ? (
<Loader />
) : error ? (
<Message variant="danger">{error}</Message>
) : (
<div>
<h1>Order: {order._id}</h1>
<Row>
<Col md={8}>
<ListGroup variant="flush">
<ListGroup.Item>
<h2>Shipping</h2>
<p>
<strong>Name: </strong>
{order.user.name}
</p>
<p>
<strong>Email: </strong>
<a href={`mailto:${order.user.email}`}>{order.user.email}</a>
</p>
<p>
<strong>Shipping: </strong>
{order.shippingAddress.address}, {order.shippingAddress.city}
{" "}
{order.shippingAddress.postalCode},{" "}
{order.shippingAddress.country}
</p>
{order.isDelivered ? (
<Message variant="success">
Delivered on {order.deliveredAt}
</Message>
) : (
<Message variant="warning">Not Delivered</Message>
)}
</ListGroup.Item>
<ListGroup.Item>
<h2>Payment Method</h2>
<p>
<strong>Method: </strong>
{order.paymentMethod}
</p>
{order.isPaid ? (
<Message variant="success">Paid on {order.paidAt}</Message>
) : (
<Message variant="warning">Not Paid</Message>
)}
</ListGroup.Item>
<ListGroup.Item>
<h2>Order Items</h2>
{order.orderItems.length === 0 ? (
<Message variant="info">Your order is empty</Message>
) : (
<ListGroup variant="flush">
{order.orderItems.map((item, index) => (
<ListGroup.Item 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).toFixed(2)}
</Col>
</Row>
</ListGroup.Item>
))}
</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>${order.itemsPrice}</Col>
</Row>
</ListGroup.Item>
<ListGroup.Item>
<Row>
<Col>Shipping:</Col>
<Col>${order.shippingPrice}</Col>
</Row>
</ListGroup.Item>
<ListGroup.Item>
<Row>
<Col>Tax:</Col>
<Col>${order.taxPrice}</Col>
</Row>
</ListGroup.Item>
<ListGroup.Item>
<Row>
<Col>Total:</Col>
<Col>${order.totalPrice}</Col>
</Row>
</ListGroup.Item>
{!order.isPaid && (
<ListGroup.Item>
{loadingPay && <Loader />}
{!sdkReady ? (
<Loader />
) : (
<PayPalButton
amount={order.totalPrice}
onSuccess={successPaymentHandler}
/>
)}
</ListGroup.Item>
)}
</ListGroup>
</Card>
</Col>
</Row>
</div>
);
}
export default OrderScreen;
Here everything works perfectly fine, but when you decide to go from this page to the Home page:
import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Row, Col } from "react-bootstrap";
import Product from "../components/Product";
import Loader from "../components/Loader";
import Message from "../components/Message";
import { listProducts } from "../actions/productActions";
function HomeScreen() {
const dispatch = useDispatch();
const productList = useSelector((state) => state.productList);
const { error, loading, products } = productList;
useEffect(() => {
dispatch(listProducts());
}, [dispatch]);
return (
<div>
<h1>Latest Products</h1>
{loading ? (
<Loader />
) : error ? (
<Message variant="danger">{error}</Message>
) : (
<Row>
{products.map((product) => (
<Col key={product._id} sm={12} md={6} lg={4} xl={3}>
<Product product={product} />
</Col>
))}
</Row>
)}
</div>
);
}
export default HomeScreen;
You get the Invariant failed error at line 13, where I'm trying to dispatch the listProducts method.
I've had a situation with this error not so long ago, managed to fix it because I used to update a state directly. I understand why I used to get that error before, but this time I am way more confused than before, because this time I am literally not touching any value anywhere along the state, I am just displaying it. I've checked the backend and I get no errors there, meaning this is strictly a React problem. I do not understand why the problem is occurring only when I am trying to go from details to the home screen.