Integrated redux now products aren't showing anymore. I tried adding a condition like "{products && products.map((product) => ..")
. A blank page disappears but still no products. The error disappears as well though. In the redux devTools I get:
{
type: 'PRODUCT_LIST_SUCCESS',
payload: [
{
_id: 1,
name: 'IPhone 11 Pro 256GB memory',
image: '/images/phone.jpg',
brand: 'Apple',
category: 'Electronics',
description: 'Bluetooth technology lets you connect it with compatible devices wirelessly High-Quality AAC audio offers immense experience Built-in microphone allows you take calls while working',
rating: '4.00',
numReviews: 8,
price: '599.99',
countInStock: 7,
created: '2022-11-14T18:29:17.417487Z',
user: 1
},
]
}
store.js:
import {
legacy_createStore as createStore,
combineReducers,
applyMiddleware,
} from "redux";
//import { configureStore } from "@reduxjs/toolkit";
import thunk from "redux-thunk";
import { composeWithDevTools } from "redux-devtools-extension";
import {
productListReducers,
productDetailsReducers,
} from "./reducers/ProductReducers";
const reducer = combineReducers({
productList: productListReducers,
productDetails: productDetailsReducers,
});
const initialState = {};
const middleware = [thunk];
const store = createStore(
reducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
export default store;
app.js:
import "./App.css";
import "./index.css";
import { Container } from "react-bootstrap";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import Footer from "./components/Footer";
import Header from "./components/Header";
import HomeScreen from "./components/screens/HomeScreen";
import ProductScreen from "./components/screens/ProductScreen";
function App() {
return (
<Router>
<Header />
<main className="my-3">
<Container>
<Routes>
<Route path="/" element={<HomeScreen />} />
<Route path="/product/:id" element={<ProductScreen />} />
</Routes>
</Container>
</main>
<Footer />
</Router>
);
}
export default App;
HomeScreen.js:
import React, { useState, useEffect } from "react";
//import products from "../../products";
import { Row, Col } from "react-bootstrap";
import Product from "../Product";
import { useDispatch, useSelector } from "react-redux";
import { listProducts } from "../../actions/ProductAction";
import Message from "../Message";
import Loader from "../Loader";
function HomeScreen() {
const dispatch = useDispatch();
const productList = useSelector((state) => state.productList);
const { error, loading, products } = productList;
useEffect(() => {
dispatch(listProducts());
}, [dispatch]);
return (
<div>
<h1 className="text-center">Latest Products</h1>
{loading ? (
<Loader />
) : error ? (
<Message variant="danger">{error}</Message>
) : (
<Row>
{products &&
products.map((product) => (
<Col key={product._id} sm={12} md={6} lg={4} xl={3}>
{/* <h3>{product.name}</h3> */}
<Product product={product} />
</Col>
))}
</Row>
)}
</div>
);
}
export default HomeScreen;
ProductScreen.js:
import React, { useState, useEffect } from "react";
import { Link, useParams } from "react-router-dom";
import { Row, Col, Image, ListGroup, Button, Card } from "react-bootstrap";
//import products from "../../products";
import Rating from "../Rating";
import { useDispatch, useSelector } from "react-redux";
import Message from "../Message";
import Loader from "../Loader";
import { listProductDetails } from "../../actions/ProductAction";
import { productDetailsReducers } from "../../reducers/ProductReducers";
function ProductScreen() {
const [quantity, setQuantity] = useState(1);
const { id } = useParams();
const dispatch = useDispatch();
const productDetails = useSelector((state) => state.productDetails);
const { loading, error, product } = productDetails;
useEffect(() => {
async function fetchProduct() {
if (id) {
dispatch(listProductDetails(id));
}
}
fetchProduct();
}, [dispatch, id]);
return (
<div>
<Link to="/" className="btn btn-dark my-3">
Go Back
</Link>
{loading ? (
<Loader />
) : error ? (
<Message variant="danger">{error}</Message>
) : (
<Row>
<Col md={6}>
<Image
className="imgCustom"
src={product.image}
alt={product.name}
></Image>
</Col>
<Col md={3}>
<ListGroup variant="flush">
<ListGroup.Item>
<h3>{product.name}</h3>
</ListGroup.Item>
<ListGroup.Item>
<Rating
value={product.rating}
text={`${product.numReviews} reviews`}
color={"f8e825"}
/>
</ListGroup.Item>
<ListGroup.Item>Price : ${product.price}</ListGroup.Item>
<ListGroup.Item>
Description: ${product.description}
</ListGroup.Item>
</ListGroup>
</Col>
<Col md={3}>
<Card>
<ListGroup variant="flush">
<ListGroup.Item>
<Row>
<Col>Price : </Col>
<Col>
<strong>$ {product.price}</strong>
</Col>
</Row>
</ListGroup.Item>
<ListGroup.Item>
<Row>
<Col>Status : </Col>
<Col>
{product.countInStock > 0 ? "In Stock" : "Out of Stock"}
</Col>
</Row>
</ListGroup.Item>
<ListGroup.Item>
<Button
className="btn-block"
disabled={product.countInStock === 0}
type="button"
>
Add to Cart
</Button>
</ListGroup.Item>
</ListGroup>
</Card>
</Col>
</Row>
)}
</div>
);
}
export default ProductScreen;
ProductAction.js:
import axios from "axios";
import {
PRODUCT_LIST_REQUEST,
PRODUCT_LIST_SUCCESS,
PRODUCT_LIST_FAIL,
PRODUCT_DETAILS_REQUEST,
PRODUCT_DETAILS_SUCCESS,
PRODUCT_DETAILS_FAIL,
} from "../constants/ProductConstants";
export const listProducts = () => async (dispatch) => {
try {
dispatch({ type: PRODUCT_LIST_REQUEST });
const { data } = await axios.get("/api/products/");
dispatch({
type: PRODUCT_LIST_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: PRODUCT_LIST_FAIL,
payload:
error.response && error.response.data.detail
? error.response.data.detail
: error.message,
});
}
};
export const listProductDetails = (id) => async (dispatch) => {
try {
dispatch({ type: PRODUCT_DETAILS_REQUEST });
const { data } = await axios.get(`/api/products${id}`);
dispatch({
type: PRODUCT_DETAILS_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: PRODUCT_DETAILS_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
ProductReducers.js:
import {
PRODUCT_LIST_REQUEST,
PRODUCT_LIST_SUCCESS,
PRODUCT_LIST_FAIL,
PRODUCT_DETAILS_REQUEST,
PRODUCT_DETAILS_SUCCESS,
PRODUCT_DETAILS_FAIL,
} from "../constants/ProductConstants";
export const productListReducers = (state = { products: [] }, action) => {
switch (action.type) {
case PRODUCT_LIST_REQUEST:
return { loading: true, product: [] };
case PRODUCT_LIST_SUCCESS:
return { loading: false, product: [action.payload] };
case PRODUCT_LIST_FAIL:
return { loading: true, product: [action.payload] };
default:
return state;
}
};
export const productDetailsReducers = (state = {product:{ reviews: [] }}, action) => {
switch (action.type) {
case PRODUCT_DETAILS_REQUEST:
return { loading: true, ...state };
case PRODUCT_DETAILS_SUCCESS:
return { loading: false, product: [action.payload] };
case PRODUCT_DETAILS_FAIL:
return { loading: true, product: [action.payload] };
default:
return state;
}
};
CodePudding user response:
Issue
It appears the HomeScreen
component is selecting out a products
property that doesn't exist in the redux store state slice after the state is updated at least once.
HomeScreen
function HomeScreen() {
const dispatch = useDispatch();
const productList = useSelector((state) => state.productList);
const { error, loading, products } = productList; // <-- products
Redux store's combined reducers
const reducer = combineReducers({
productList: productListReducers,
productDetails: productDetailsReducers,
});
productListReducers
None of the reducer cases here maintain the products
array state invariant, they each change/return a product
property.
export const productListReducers = (state = { products: [] }, action) => {
switch (action.type) {
case PRODUCT_LIST_REQUEST:
return { loading: true, product: [] };
case PRODUCT_LIST_SUCCESS:
return {
loading: false,
product: [action.payload] // <-- product, not products
};
case PRODUCT_LIST_FAIL:
return { loading: true, product: [action.payload] };
default:
return state;
}
};
Additionally the PRODUCT_LIST_SUCCESS
action appears to return a payload that is a products array.
{
type: 'PRODUCT_LIST_SUCCESS',
payload: [
{
_id: 1,
name: 'IPhone 11 Pro 256GB memory',
image: '/images/phone.jpg',
brand: 'Apple',
category: 'Electronics',
description: 'Bluetooth technology lets you connect it with compatible devices wirelessly High-Quality AAC audio offers immense experience Built-in microphone allows you take calls while working',
rating: '4.00',
numReviews: 8,
price: '599.99',
countInStock: 7,
created: '2022-11-14T18:29:17.417487Z',
user: 1
},
]
}
return { loading: false, product: [action.payload] };
would result in nesting an array within an array, which is likely not what was intended.
Solution
Maintain the products
property state invariant and set the products
state to be the value of the action.payload
. You very also likely want to clear the products
array and set some error state in the case of failure.
export const productListReducers = (state = { products: [] }, action) => {
switch (action.type) {
case PRODUCT_LIST_REQUEST:
return { loading: true, products: [] };
case PRODUCT_LIST_SUCCESS:
return { loading: false, products: action.payload };
case PRODUCT_LIST_FAIL:
return {
loading: false, // <-- clear loading state
products: [], // <-- empty products array
error: action.payload, // <-- set error state
};
default:
return state;
}
};