-Hello guys , I've encountered a problem which taken a lot time in research but I did not get a solution:
->here I have 2 separate components header.jsx(contains booking.com homepage clone) and hotel.jsx and I want to send a state containing search data from header to hotel using context and use reducer . I've declared a separate context file searchcontext.js ,exported the context and its provider , wrapped the app using the provider in index.js , and I've called the dispatch function in header.js in order to send the needed state , when I call useContext in hotel.js to get the state the console return undefined ,any help please :)
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { SearchContextProvider } from './context/searchcontext';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<SearchContextProvider>
<App />
</SearchContextProvider>
</React.StrictMode>
);
searchcontext.js
import { createContext, useReducer } from "react";
const INITIAL_STATE = {
city: undefined,
dates: [],
options: {
adult: undefined,
children: undefined,
room: undefined,
},
};
export const SearchContext = createContext(INITIAL_STATE);
const searchReducer = (action, state) => {
switch (action.type) {
case "NEW_SEARCH":
return action.payload;
case "RESET_SEARCH":
return INITIAL_STATE;
default:
return state;
}
};
export const SearchContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(searchReducer, INITIAL_STATE);
return (
<SearchContext.Provider
value={{
city: state.city,
dates: state.dates,
options: state.options,
dispatch,
}}
>
{children}
</SearchContext.Provider>
);
};
header.jsx file
import "./header.css";
import { DateRange } from "react-date-range";
import { useContext, useState } from "react";
import "react-date-range/dist/styles.css"; // main css file
import "react-date-range/dist/theme/default.css"; // theme css file
import { format } from "date-fns";
import { useNavigate } from "react-router-dom";
import {SearchContext} from "../../context/searchcontext";
const Header = ({ type }) => {
const [destination, setDestination] = useState("");
const [openDate, setOpenDate] = useState(false);
const [dates, setDates] = useState([
{
startDate: new Date(),
endDate: new Date(),
key: "selection",
},
]);
const [openOptions, setOpenOptions] = useState(false);
const [options, setOptions] = useState({
adult: 1,
children: 0,
room: 1,
});
const navigate = useNavigate();
const handleOption = (name, operation) => {
setOptions((prev) => {
return {
...prev,
[name]: operation === "i" ? options[name] + 1 : options[name] - 1,
};
});
};
const{dispatch}= useContext(SearchContext);
const handleSearch = () => {
dispatch({type:"NEW_SEARCH",payload:{destination,dates,options}});
console.log(true)
navigate("/hotels", { state: { destination, dates, options } });
};
return (
<div className="header">
<div className="headerContainer" >
<div className="headerSearch">
<div className="headerSearchItem">
<input
type="text"
placeholder="where are you going?"
className="headerSearchInput"
onChange={(e) => setDestination(e.target.value)}
/>
</div>
<div className="headerSearchItem">
<span
onClick={() => setOpenDate(!openDate)}
className="headerSearchText"
>
{`${format(dates[0].startDate, "dd/MM/yyyy")} to ${format(
dates[0].endDate,
"dd/MM/yyyy"
)}`}
</span>
{openDate && (
<DateRange
editableDateInputs={true}
onChange={(item) => setDates([item.selection])}
moveRangeOnFirstSelection={false}
ranges={dates}
minDate={new Date()}
className="date"
/>
)}
</div>
<div className="headerSearchItem">
<span
onClick={() => setOpenOptions(!openOptions)}
className="headerSearchText">
{`${options.adult}`} adult {`${options.children}`} children
{`${options.room}`} room
</span>
{openOptions && (
<div className="options">
<div className="option">
<span className="optionTitle">Adults</span>
<div className="optionCounter">
<button
className="optionCounterButton"
onClick={() => {
handleOption("adult", "d");
}}
disabled={options.adult <= 1}
>
-
</button>
<span>{options.adult}</span>
<button
className="optionCounterButton"
onClick={() => {
handleOption("adult", "i");
}}
>
+
</button>
</div>
</div>
<div className="option">
<span className="optionTitle">Children</span>
<div className="optionCounter">
<button
className="optionCounterButton"
onClick={() => {
handleOption("children", "d");
}}
disabled={options.children <= 0}
>
-
</button>
<span>{options.children}</span>
<button
className="optionCounterButton"
onClick={() => {
handleOption("children", "i");
}}
>
+
</button>
</div>
</div>
<div className="option">
<span className="optionTitle">Rooms</span>
<div className="optionCounter">
<button
className="optionCounterButton"
onClick={() => {
handleOption("room", "d");
}}
disabled={options.room <= 1}
>
-
</button>
<span>{options.room}</span>
<button
className="optionCounterButton"
onClick={() => {
handleOption("room", "i");
}}
>
+
</button>
</div>
</div>
</div>
)}
</div>
<div className="headerSearchItem">
<button className="headerBtn" onClick={handleSearch}>
Search
</button>
</div>
</div>
</>
)}
</div>
</div>
);
};
export default Header;
hotel.jsx file
import "./hotel.css";
import { useState } from "react";
import { useContext } from "react";
import { SearchContext } from "../../context/searchcontext";
const Hotel = () => {
const [sliderNumber, setSliderNumber] = useState(0);
const [openSlider, setOpenSlider] = useState(false);
//here where the problem encouters
const {state}= useContext(SearchContext);
console.log(state);//returns undefined
return (
<div style={{ margin: -8 }}>
<div className="hotelItemContainer">
<div className="hotelItemWrapper">
<button className="hotelBookBtn"> Reserve or Book Now!</button>
<h1 className="hotelTitle">lorem</h1>
<span className="hotelSpecFeatures">
You're eligible for a Genius discount! , to save at this property
, all you have to do is sign in.
</span>
</div>
</div>
</div>
);
};
export default Hotel;
In your hotel.js you are expecting a state property. However you have not been exposing one in your search context
searchcontext.js
Your context was not exposing a state
export const SearchContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(searchReducer, INITIAL_STATE);
return (
<SearchContext.Provider
value={{
city: state.city,
dates: state.dates,
options: state.options,
state, // you forgot to add this
dispatch,
}}
>
{children}
</SearchContext.Provider>
);
};
alternatively don't use the state property in hotel.js instead use the destructure values that are exposed in your context.
Related
Whenever I'm clicking on a card to go to the product details page it is automatically going to the bottom of the next page without even scrolling it here is the sample of my code
import React from "react";
import { Link } from "react-router-dom";
import { Rating } from "#material-ui/lab";
const ProductCard = ({ product }) => {
const options = {
value: product.ratings,
readOnly: true,
precision: 0.5,
};
return (
<Link className="productCard" to={`/product/${product._id}`}>
<img src={product.images[0].url} alt={product.name} />
<p>{product.name}</p>
<div>
<Rating {...options} />
<span className="productCardSpan">
{""}({product.numOfReviews} Reviews)
</span>
</div>
<span>{`₹${product.price}/-`}</span>
</Link>
);
};
export default ProductCard;
product details page is starting from here
import React, { useEffect, useState } from "react";
import Carousel from "react-material-ui-carousel";
import "./ProductDetails.css";
import { useSelector, useDispatch } from "react-redux";
import {
clearErrors,
getProductDetails,
newReview,
} from "../../actions/productAction";
import ReviewCard from "./ReviewCard.js";
import Loader from "../layout/Loader/Loader";
import { useAlert } from "react-alert";
import MetaData from "../layout/MetaData";
import { addItemsToCart } from "../../actions/cartAction";
import {
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Button,
} from "#material-ui/core";
import { Rating } from "#material-ui/lab";
import { NEW_REVIEW_RESET } from "../../constants/productConstants";
const ProductDetails = ({ match }) => {
const dispatch = useDispatch();
const alert = useAlert();
const { product, loading, error } = useSelector(
(state) => state.productDetails
);
const { success, error: reviewError } = useSelector(
(state) => state.newReview
);
const options = {
size: "large",
value: product.ratings,
readOnly: true,
precision: 0.5,
};
const [quantity, setQuantity] = useState(1);
const [open, setOpen] = useState(false);
const [rating, setRating] = useState(0);
const [comment, setComment] = useState("");
const increaseQuantity = () => {
if (product.Stock <= quantity) return;
const qty = quantity + 1;
setQuantity(qty);
};
const decreaseQuantity = () => {
if (1 >= quantity) return;
const qty = quantity - 1;
setQuantity(qty);
};
const addToCartHandler = () => {
dispatch(addItemsToCart(match.params.id, quantity));
alert.success("Item Added To Cart");
};
const submitReviewToggle = () => {
open ? setOpen(false) : setOpen(true);
};
const reviewSubmitHandler = () => {
const myForm = new FormData();
myForm.set("rating", rating);
myForm.set("comment", comment);
myForm.set("productId", match.params.id);
dispatch(newReview(myForm));
setOpen(false);
};
useEffect(() => {
if (error) {
alert.error(error);
dispatch(clearErrors());
}
if (reviewError) {
alert.error(reviewError);
dispatch(clearErrors());
}
if (success) {
alert.success("Review Submitted Successfully");
dispatch({ type: NEW_REVIEW_RESET });
}
dispatch(getProductDetails(match.params.id));
}, [dispatch, match.params.id, error, alert, reviewError, success]);
console.log(product.images);
return (
<>
{loading ? (
<Loader />
) : (
<>
<MetaData title={`${product.name} -- ECOMMERCE`} />
<div className="ProductDetails">
<div>
{product.images &&
product.images.map((item, i) => (
<img
className="CarouselImage"
key={i}
src={item.url}
alt="product"
/>
))}
</div>
<div>
<div className="detailsBlock-1">
<h2>{product.name}</h2>
<p>Product # {product._id}</p>
</div>
<div className="detailsBlock-2">
<Rating {...options} />
<span className="detailsBlock-2-span">
{" "}
({product.numOfReviews} Reviews)
</span>
</div>
<div className="detailsBlock-3">
<h1>{`₹${product.price}`}</h1>
<div className="detailsBlock-3-1">
<div className="detailsBlock-3-1-1">
<button onClick={decreaseQuantity}>-</button>
<input readOnly type="number" value={quantity} />
<button onClick={increaseQuantity}>+</button>
</div>
<button
disabled={product.Stock < 1 ? true : false}
onClick={addToCartHandler}
>
Add to Cart
</button>
</div>
<p>
Status:
<b className={product.Stock < 1 ? "redColor" : "greenColor"}>
{product.Stock < 1 ? "OutOfStock" : "InStock"}
</b>
</p>
</div>
<div className="detailsBlock-4">
Description : <p>{product.description}</p>
</div>
<button onClick={submitReviewToggle} className="submitReview">
Submit Review
</button>
</div>
</div>
<h3 className="reviewsHeading">REVIEWS</h3>
<Dialog
aria-labelledby="simple-dialog-title"
open={open}
onClose={submitReviewToggle}
>
<DialogTitle>Submit Review</DialogTitle>
<DialogContent className="submitDialog">
<Rating
onChange={(e) => setRating(e.target.value)}
value={rating}
size="large"
/>
<textarea
className="submitDialogTextArea"
cols="30"
rows="5"
value={comment}
onChange={(e) => setComment(e.target.value)}
></textarea>
</DialogContent>
<DialogActions>
<Button onClick={submitReviewToggle} color="secondary">
Cancel
</Button>
<Button onClick={reviewSubmitHandler} color="primary">
Submit
</Button>
</DialogActions>
</Dialog>
{product.reviews && product.reviews[0] ? (
<div className="reviews">
{product.reviews &&
product.reviews.map((review) => (
<ReviewCard key={review._id} review={review} />
))}
</div>
) : (
<p className="noReviews">No Reviews Yet</p>
)}
</>
)}
</>
);
};
export default ProductDetails;
I tried linking in it but it won't work and someone told me to use useRef but i don't know how to use it
You are using React router dom. In React Router there is the problem that if we redirect to the new route, it won't automatically take you to the top of the page. Such behavior is normal when you navigate between pages.
Since you are using functional components
Try to use the following window scroll to the top when the component mounts.
useEffect(() => {
window.scrollTo(0, 0)
}, [])
Browser scroll your page.
If you don't want to let browser auto scroll your page,
use History.scrollRestoration method
Prevent automatic page location restoration
if (history.scrollRestoration) {
history.scrollRestoration = 'manual';
}
Read more at MDN History.scrollRestoration
Hi I am creating an app where a user can search for a book and put it on a shelf depending on which shelf the user clicks on. Currently the user can type a query and many results can get displayed. I want the user to a dropdown on a book and click on a shelf (in the dropdown) to select that book and move it to that shelf.
What I trying to do now is retrieve the book object when the user clicks on a dropdown option (which is a shelf). I want to pass this book object into an api call. How would I retrieve the book object when the user clicks on a specific book's dropdown option? I understand that this might involve event bubbling.
I hope this question makes sense.
SearchPage.js
import React, { useEffect, useState } from 'react';
import { BsArrowLeftShort } from 'react-icons/bs';
import SearchBar from '../components/SearchBar';
import { search, update, getAll } from '../api/BooksAPI';
import Book from '../components/Book';
const SearchPage = () => {
const [query, setQuery] = useState('');
const [data, setData] = useState([]);
const handleChange = (e) => {
setQuery(e.target.value);
};
useEffect(() => {
const bookSearch = setTimeout(() => {
if (query.length > 0) {
search(query).then((res) => {
if (res.length > 0) {
setData(res);
} else setData([]);
});
} else {
setData([]);
}
}, 1000);
return () => clearTimeout(bookSearch);
}, [query]);
const [shelfType, setShelfType] = useState('None');
const [currentBook, setCurrentBook] = useState({});
const handleShelfTypeClick = (e) => {
setShelfType(e.target.value);
console.log(e.target.parentElement.parentElement.parentElement);
//here I want to retrieve the book object when the user clicks on a dropdown option (shelf)
};
return (
<div>
<SearchBar
type="text"
searchValue={query}
placeholder="Search for a book"
icon={<BsArrowLeftShort />}
handleChange={handleChange}
/>
<div className="book-list">
{data !== []
? data.map((book) => (
<Book
handleShelfTypeClick={handleShelfTypeClick}
book={book}
key={book.id}
/>
))
: 'ok'}
</div>
</div>
);
};
export default SearchPage;
Book.js
import React from 'react';
import PropTypes from 'prop-types';
import ButtonDropDown from './ButtonDropDown';
const Book = ({ book, handleShelfTypeClick }) => {
return (
<div className="book">
<img
src={book.imageLinks.thumbnail}
alt={book.title}
className="book-thumbnail"
/>
<ButtonDropDown
choices={['Currently Reading', 'Want to Read', 'Read', 'None']}
getShelfType={handleShelfTypeClick}
/>
<div className="book-title">{book.title}</div>
<div className="book-authors">{book.authors}</div>
</div>
);
};
Book.propTypes = {
handleShelfTypeClick: PropTypes.func.isRequired,
book: PropTypes.shape({
imageLinks: PropTypes.shape({
thumbnail: PropTypes.string.isRequired,
}),
title: PropTypes.string.isRequired,
authors: PropTypes.arrayOf(PropTypes.string),
}).isRequired,
};
export default Book;
ButtonDropDown.js
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { BsFillCaretDownFill } from 'react-icons/bs';
const ButtonDropDown = ({ choices, label, getShelfType }) => {
const [active, setActive] = useState(false);
const toggleClass = () => {
setActive(!active);
};
return (
<div className="dropdown">
<button
type="button"
className="dropbtn"
onFocus={toggleClass}
onBlur={toggleClass}
>
<BsFillCaretDownFill />
</button>
<div
id="myDropdown"
className={`dropdown-content ${active ? `show` : `hide`}`}
>
<div className="dropdown-label">{label}</div>
{choices.map((choice, index) => (
<button
// eslint-disable-next-line react/no-array-index-key
key={index}
className="dropdown-choice"
onClick={getShelfType}
type="button"
value={choice}
>
{choice}
</button>
))}
</div>
</div>
);
};
ButtonDropDown.propTypes = {
choices: PropTypes.arrayOf(PropTypes.string).isRequired,
label: PropTypes.string,
getShelfType: PropTypes.func.isRequired,
};
ButtonDropDown.defaultProps = {
label: 'Move to...',
};
export default ButtonDropDown;
You are focusing on the signature for the onClick event, but you can actually pass a callback with any format that you need and then build onClick dinamically.
For instance, in Book you could have a callback that receives book and shelf:
const Book = ({ book, doSomethingWithBookAndShelf }) => {
return (
<div className="book">
<img
src={book.imageLinks.thumbnail}
alt={book.title}
className="book-thumbnail"
/>
<ButtonDropDown
choices={['Currently Reading', 'Want to Read', 'Read', 'None']}
onSelectChoice={(choice) => {
// book came from the component props
doSomethingWithBookAndShelf(book, choice);
}}
/>
<div className="book-title">{book.title}</div>
<div className="book-authors">{book.authors}</div>
</div>
);
};
And in ButtonDropDown:
const ButtonDropDown = ({ choices, label, onSelectChoice }) => {
const [active, setActive] = useState(false);
const toggleClass = () => {
setActive(!active);
};
return (
<div className="dropdown">
<button
type="button"
className="dropbtn"
onFocus={toggleClass}
onBlur={toggleClass}
>
<BsFillCaretDownFill />
</button>
<div
id="myDropdown"
className={`dropdown-content ${active ? `show` : `hide`}`}
>
<div className="dropdown-label">{label}</div>
{choices.map((choice, index) => (
<button
// eslint-disable-next-line react/no-array-index-key
key={index}
className="dropdown-choice"
onClick={() => { // we create an specific callback for each item
onSelectChoice(choice);
}}
type="button"
value={choice}
>
{choice}
</button>
))}
</div>
</div>
);
};
Hope that puts you in some direction.
Also, beware it is more React-like to work like that. Avoid using the event objects to get values (i.e. e.target.value).
I'm trying to edit an input value in a child component and send to the parent
:
https://codesandbox.io/s/sleepy-rain-skoss?file=/src/Editlabel.js:0-389
Parent:
import "./styles.css";
import EditLabel from "./Editlabel";
import { useEffect, useState } from "react";
export default function App() {
const [newName, setNewName] = useState();
useEffect(() => {
console.log("newName", newName);
}, [newName]);
return (
<div className="App">
<EditLabel
value={"hello"}
click={(changedName) => {
setNewName(changedName);
}}
/>
</div>
);
}
Child:
import React, { useState } from "react";
const EditLabel = ({ value, click }) => {
const [name, setName] = useState(value);
return (
<>
<input type={"text"} placeholder={name}></input>
<button
onClick={(e) => {
setName(e.target.value);
click(name);
}}
>
Edit
</button>
</>
);
};
export default EditLabel;
However, the console logs "hello" and then it just logs empty strings.
How can I make it work?
try this on your child's input box
<input type={"text"} placeholder={name} onChange={(e) => setName(e.target.value)}>
Change EditLabel to use a ref to capture the input value:
const EditLabel = ({ value, click }) => {
const inputRef = useRef(null);
return (
<>
<input ref={inputRef} type={"text"} placeholder={value}></input>
<button
onClick={() => {
click(inputRef.current.value);
}}
>
Edit
</button>
</>
);
};
Update App to use the values it gets via the click callback:
export default function App() {
const [newName, setNewName] = useState("hello");
useEffect(() => {
console.log("newName", newName);
}, [newName]);
return (
<div className="App">
<EditLabel
value={newName}
click={(changedName) => {
setNewName(changedName);
}}
/>
</div>
);
}
This block of code:
const data = {
user_id: userID,
comment: comment
};
const url = `http://localhost:5000/blog/comments${category}/${slug}`;
const headers = { "Access-Control-Allow-Origin": "*" };
axios.post(url, data, headers).then(resp => {
console.log(resp.data.message);
});
}
is giving me this error:
Unhandled Rejection (TypeError): cyclic object value
from what I've gathered so far "cyclic object value" refers to javascript objects that at some point refer to itself hence throws an error. From the code above it's not very obvious how I accomplished such error.
I would appreciated any pointers here.
Attempt to provide a minimal example:
Post.jsx:
import React, { useState, useEffect } from "react";
import { useGlobalContext } from "../context";
import { Link } from "react-router-dom";
import AddComment from "./AddComment";
import axios from "axios";
const Post = props => {
const { userID, trace, setTrace, isAuthenticated } = useGlobalContext();
const [obj, setObj] = useState([]);
const [comments, setComments] = useState([]);
const [comment, setComment] = useState("");
const [showLoginWarning, setShowLoginWarning] = useState(false);
const { firstColumn, secondColumn, slug, category } = props;
const handleComment = e => {
e.preventDefault();
if (!isAuthenticated) {
setShowLoginWarning(true);
} else {
const data = {
user_id: userID,
comment: comment
};
const url = `http://localhost:5000/blog/comments${category}/${slug}`;
const headers = { "Access-Control-Allow-Origin": "*" };
axios
.post(url, data, headers)
.then(resp => {
console.log(resp.data.message);
})
.catch(err => {
console.log(err);
});
}
};
useEffect(() => {
setTrace(category + `/${slug}`);
//console.log(trace);
let obj = firstColumn.filter(post => post.slug === slug);
if (obj.length === 0) {
obj = secondColumn.filter(post => post.slug === slug);
}
setObj(obj);
const headers = { "Access-Control-Allow-Origin": "*" };
const url = `http://localhost:5000/blog/comments${trace}/${slug}`;
axios
.get(url)
.then(resp => {
setComments(resp.data.message);
console.log(resp.data.message);
})
.catch(err => {
console.log(err);
});
}, []);
useEffect(() => {
const timeout = setTimeout(() => {
setShowLoginWarning(false);
}, 7000);
}, [showLoginWarning]);
const img_file = `http://localhost:5000/blog/fetch_image/post/${slug}`;
return (
<div style={{ marginTop: "40px" }} className="container">
<div className="columns">
<div className="column is-four-fifths">
<div className="content">
{obj.map(prop => (
<div key={prop.id}>
<h4 className="title is-4">
<img src={img_file} alt={prop.title} />
<br />
{prop.title}
<small>{prop.timestamp}</small>
</h4>
<br />
<p>{prop.post}</p>
</div>
))}
</div>
</div>
<div className="column">
<br />
<Link to={category} className="button is-link" id="backbtn">
Back
</Link>
</div>
</div>
<section className="section">
<h1 className="title">Section</h1>
<h2 className="subtitle">
A simple container to divide your page into <strong>sections</strong>,
like the one you're currently reading.
</h2>
<AddComment
onComment={handleComment}
showLoginWarning={showLoginWarning}
onChange={setComment}
/>
</section>
</div>
);
};
export default Post;
AddComment.jsx:
import React from "react";
const AddComment = props => {
const { showLoginWarning, onComment, onChange } = props;
return (
<>
{showLoginWarning && (
<div className="notification is-warning">
<strong>You need to be logged in to comment.</strong>
</div>
)}
<section id="textareaSection" className="section">
<form onSubmit={onComment}>
<div className="field">
<div className="control">
<textarea
className="textarea"
cols="2"
rows="2"
placeholder="What's On Your Mind ?"
onChange={onChange}
/>
<button
type="submit"
style={{ float: "right", marginTop: "1em" }}
className="button is-link"
>
Post
</button>
</div>
</div>
</form>
</section>
</>
);
};
export default AddComment;
index.js:
import React from "react";
import ReactDOM from "react-dom";
import "./assets/bulma/css/bulma.css";
import "./assets/stylesheet.css";
import Post from "./Post";
import reportWebVitals from "./reportWebVitals";
import { AppProvider } from "./context";
ReactDOM.render(
<React.StrictMode>
<AppProvider>
<Post />
</AppProvider>
</React.StrictMode>,
document.getElementById("root")
);
reportWebVitals();
As soon as my app loads the 'FETCH_PRODUCTS' action payload is dispatched, resulting in items sourced from the data.json file, being added to state in my products reducer. I am able to access this state
via console.log(action.payload) in both my actions and reducers files. I need to be able to iterate through the state object so I can render products from state in my Products component. However, I am unable to iterate thru the object. Ive tried with no luck, to convert it to an iterable array with Object.keys(), .values() and .entries().
This is what I get when console.log() action.payload or products in Products.js
Products.js
import React, { useState, useEffect } from "react";
import { Fade, Zoom } from "react-reveal";
import Modal from "react-modal";
import { connect, useDispatch } from "react-redux";
import fetchProducts from "../actions/productActions";
const Products = ({ products, add }) => {
const [product, setProduct] = useState(null);
const openModal = (product) => {
setProduct(product);
};
const closeModal = () => {
setProduct(null);
};
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchProducts());
}, [dispatch]);
console.log(products)
const renderProducts = () => {
return products.map((product) => {
return (
<li key={product._id}>
<a href={"#" + product._id} onClick={() => openModal(product)}>
<img src={product.image} />
</a>
<p>
{product.title}
</p>
<p>
<strong>${product.price}.00</strong>
</p>
<button onClick={() => add(product)}>ADD TO CART</button>
</li>
);
});
};
return (
<div>
<Fade bottom cascade>
<div>Loading...</div>
<ul>{!products ? <div>Loading...</div> : renderProducts()}</ul>
</Fade>
{product && (
<Modal isOpen={true}>
<Zoom clear cascade>
<div>
<p onClick={() => closeModal()}>X CLOSE</p>
<img src={product.image} />
</div>
<div>
<p>{product.title}</p>
<p>${product.price}.00</p>
Sizes
<p>
Available Sizes
{product.availableSizes.map((size) => {
return (
<>
<br />
<span> {size} </span>
</>
);
})}
</p>
<button
onClick={() => {
add(product);
closeModal();
}}
>
ADD TO CART
</button>
</div>
</Zoom>
</Modal>
)}
</div>
);
};
export default connect((state) => ({ products: state.products.items }), {
fetchProducts,
})(Products);
productActions.js
import { FETCH_PRODUCTS } from "../components/types";
const fetchProducts = () => async(dispatch) => {
const result = await fetch('data/data.json');
const data = await result.json();
console.log(data);
dispatch({
type: FETCH_PRODUCTS,
payload: data
})
}
export default fetchProducts
productReducers.js
const { FETCH_PRODUCTS } = require("../components/types");
const data = require('../data.json')
const productsReducer = (state = {}, action) =>{
switch(action.type){
case FETCH_PRODUCTS:
console.log(action.payload)
return {items: action.payload}
default:
return state;
}
}
export default productsReducer;
store.js
import { createStore, applyMiddleware, compose, combineReducers } from "redux";
import thunk from "redux-thunk";
import productsReducer from "./reducers/productReducers";
const initialState = {};
const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
combineReducers({ products: productsReducer }),
initialState,
composeEnhancer(applyMiddleware(thunk))
);
export default store;
You aren't accessing the data, because you didn't get the property from the object
Your redux store for products says this whenever you console.log(products)
{products : Array(6)}
So as a result you have to say products.products to properly map the array
Addressing undefined value
The problem is you are instantly returning the component when there is no data
const renderProducts = () => {
return products.map((product) => {
return (
<li key={product._id}>
<a href={"#" + product._id} onClick={() => openModal(product)}>
<img src={product.image} />
</a>
<p>
{product.title}
</p>
<p>
<strong>${product.price}.00</strong>
</p>
<button onClick={() => add(product)}>ADD TO CART</button>
</li>
);
});
};
Keep in mind that getting data takes time so you have to check if your products state is empty. This can be solve with a ternary operator:
return (
<div>
{doSomething ? something : null}
</div>
);
So in your case check if the products array is empty. If yes map through the array. If no then return "no products".
return (
<div>
{products.products.length !== 0 ? <your regular routine as above) : <p>No products</p>}
</div>
);
Finally figured it out!!! Needed to change payload: data to payload: data.products in productActions.js