I am trying to fetch my data inside useEffect but every time i get an empty array when i try to send it as props to another component (Product)
ProducList.js
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import { fetchProducts } from "../actions/products";
import { Product } from "./Product";
const ProductList = ({ getProducts, products, loading }) => {
useEffect(() => {
getProducts();
}, []);
return (
<div className="p-4">
<Product products={data} />
</div>
);
};
const mapStateToProps = state => ({
products: state.products,
loading: state.loading
});
const mapDispatchToProps = {
getProducts: fetchProducts
};
export default connect(mapStateToProps, mapDispatchToProps)(ProductList);
and here my Product.js
import React from "react";
export const Product = props => {
const products = props.products.map(product => {
return (
<div className="col-lg-4 mb-4" key={product.Id}>
<div className="card shadow-sm">
<img
className="card-img-top"
src={`/images/${product.Id}.jpg`}
alt={product.name}
/>
<div className="card-body">
<h5 className="card-title">
{product.name}{" "}
<span className="badge badge-warning">${product.price}</span>
</h5>
<a href="#" className="btn btn-secondary mx-auto">
Add to cart
</a>
</div>
</div>
</div>
);
});
return <div className="row">{products}</div>;
};
i need to fetch data and send to product component
It looks like your data is never passed into the ProductList component (there is no reference to a data prop in ProductList).
I believe you are trying to pass the products data into the Products component. Here's what you could do.
const [newProducts, setNewPropducts] = useState([]);
//add this useEffect and keep your other one
useEffect(() => {
//set state for products here
setProducts(products)
}, [products]);
<Product products={newProducts} />
Related
This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 16 days ago.
I make an ecommerce site
I am trying to display the page with the details of a product:
import React, { useState, useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import axios from "axios";
import Layout from "../components/Layout";
const ProductPage = () => {
const router = useRouter();
const [product, setProduct] = useState({});
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchProduct = async () => {
try {
const response = await axios.get(`/api/products?id=${router.query.id}`);
const currentProduct = response.data;
setLoading(false);
setProduct(currentProduct);
} catch (error) {
console.error(error);
}
};
fetchProduct();
}, [router.query.id]);
return (
<Layout title={product.name}>
{loading ? (
<div className="text-center">Loading...</div>
) : (
<div className="max-w-lg mx-auto p-5">
<h1 className="text-2xl font-bold mb-5">{product.name}</h1>
<img
src={product.imageUrl}
alt={product.name}
className="w-full mb-5"
/>
<p className="mb-5">{product.description}</p>
<p className="text-xl font-bold mb-5">
Price: {product.price}
</p>
<Link href="/" legacyBehavior>
<a className="btn btn-primary">Go back to the products list</a>
</Link>
</div>
)}
</Layout>
);
};
export default ProductPage;
The values are not displayed so I put some console.log
When I put a console.log in the "try" product returns an empty object, and when I put the console.log after the useffect the product returns an object with my values
You need to set the "isLoading" state in false whene the "product" state when the state has finished setting. In order to do that you need to add 1 more useEffect to listen for changes in the product state, for that you must add the "product" state as a dependency of the useEffect.
Try the next code:
import React, { useState, useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import axios from "axios";
import Layout from "../components/Layout";
const ProductPage = () => {
const router = useRouter();
const [product, setProduct] = useState({});
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchProduct = async () => {
try {
const response = await axios.get(`/api/products?id=${router.query.id}`);
const currentProduct = response.data;
setProduct(currentProduct);
} catch (error) {
console.error(error);
}
};
fetchProduct();
}, [router.query.id]);
useEffect(() => {
setLoading(false);
}, [product]);
return (
<Layout title={product.name}>
{loading ? (
<div className="text-center">Loading...</div>
) : (
<div className="max-w-lg mx-auto p-5">
<h1 className="text-2xl font-bold mb-5">{product.name}</h1>
<img
src={product.imageUrl}
alt={product.name}
className="w-full mb-5"
/>
<p className="mb-5">{product.description}</p>
<p className="text-xl font-bold mb-5">Price: {product.price}</p>
<Link href="/" legacyBehavior>
<a className="btn btn-primary">Go back to the products list</a>
</Link>
</div>
)}
</Layout>
);
};
export default ProductPage;
useState hook does not update the state value instantly. There are a lot of things happening when you change the state, like batching state updates, recomputing DOM manipulations, re-rendering components, etc.
If you want to know what is going to be set as your state, log the argument that you are passing to the setProduct function. You may also use React dev tools.
i have an ojbect inside an object and i want to extract the sub object to map it but the component is readered before the data is arrived
the main object name is Movie and the sub object is Categories
and i want to map Categories but it says undefind.
import Card from '../UI/Card';
import classes from './MovieDetails.module.css';
import MovieCategoris from './MovieCategories';
import ReactPlayerProps from "react-player";
const MovieDetails = (props) => {
const Movie = props.Movie
const image = Movie.Image;
const Categories = Movie.Categories
const videoSrc = Movie.Video;
return (
<Card>
<div className={classes.VideoPlayer}>
<ReactPlayerProps
controls
volume={1}
muted={false}
width={"100%"}
height={"100%"}
url={videoSrc}
/>
</div>
<div className={classes.DetailsBox}>
<div className={classes.Details}>
<div className={classes.Title}>
<img src={image} />
</div>
<div className={classes.MovDet}>
<h3>{Movie.Title}</h3>
<ul>
<li>Duration: <label>{Movie.Duration}</label></li>
<li>Quality: <label>HD 720</label></li>
<li>release date: <label>{Movie.Year}</label></li>
<li>IMBb: <label>{Movie.Rate}</label></li>
</ul>
<h5>Categories</h5>
{/* <div>
<ul className={classes.Cat_li}>
{Categories.map((cat) =>
<li>{cat}</li>
)}
</ul>
</div> */}
</div>
<div className={classes.Desc}>
<p> {Movie.Description} </p>
</div>
</div>
</div>
</Card>
)
}
export default MovieDetails;
this is the function that get the api data and forward it with props to the component.
import MovieDetails from "../components/Content/MovieDetails";
import { useParams } from "react-router-dom";
import { useEffect, useState } from "react";
import FetchedMovie from '../Hooks/fetchSingleMovie'
const MovieDetailsPage = () => {
const [MovieData, setMovieData] = useState([])
const MovieId = useParams()
async function Movie(MovieId) {
const id = MovieId.movie
const result = await FetchedMovie(id)
setMovieData(result)
return result
}
useEffect(() => {
Movie(MovieId)
}, [])
return <MovieDetails Movie={MovieData} />
}
export default MovieDetailsPage;
this is the object i have on the api sever
That's because you ara passing an empty array in your MovieDetails component in MovieDetailsPage
you can render MovieDetails whene your data is ready
Try to change it in something like this
import MovieDetails from "../components/Content/MovieDetails";
import { useParams } from "react-router-dom";
import { useEffect, useState } from "react";
import FetchedMovie from '../Hooks/fetchSingleMovie'
const MovieDetailsPage = () => {
const [MovieData, setMovieData] = useState(null)
const MovieId = useParams()
async function Movie(MovieId) {
const id = MovieId.movie
const result = await FetchedMovie(id)
setMovieData(result)
return result
}
useEffect(() => {
Movie(MovieId)
}, [])
return MovieData && <MovieDetails Movie={MovieData} />
}
export default MovieDetailsPage;
it happens because the api will render after your first moviedetails call;
you can use an if before your map that everytime its defined, map the function like this: {categories && categories.map ... }; because everytime that the props changes page will rerender;
I'm stucked... :)
I have a single view in Laravel where React appends to the specific ID. What I try to do is to open the pop-up after click the button using an useContext. Below is my code:
Globalcontext.js
import React from 'react';
export const initialState = {
usersData: null,
clickedNewUserButton: false,
showAddUserPopup: false,
isLoading: true,
};
export const GlobalContext = React.createContext(initialState);
UsersPageMain.js
import React, { useEffect, useContext } from 'react';
import ReactDOM from 'react-dom';
import { GlobalContext } from '../state/GlobalContext';
import FiltersButton from '../components/Users/FiltersButton';
import AddUserButton from '../components/Users/AddUserButton';
import UsersTable from '../components/Users/UsersTable';
import AddNewUserPopup from '../components/Users/AddNewUserPopup';
function UsersPageMain(){
const initialState = useContext(GlobalContext);
if(initialState.clickedNewUserButton){
return (
<GlobalContext.Provider value={initialState}>
<div className='container users-list-page'>
<div className='row'>
<FiltersButton/>
<AddUserButton/>
</div>
<div className='row'>
<UsersTable></UsersTable>
</div>
</div>
<AddNewUserPopup/>
</GlobalContext.Provider>
)
}else{
return (
<GlobalContext.Provider value={initialState}>
<div className='container users-list-page'>
<div className='row'>
<FiltersButton/>
<AddUserButton/>
</div>
<div className='row'>
<UsersTable></UsersTable>
</div>
</div>
</GlobalContext.Provider>
)
}
}
export default UsersPageMain;
if (document.getElementById('user-list-page')) {
ReactDOM.render(<UsersPageMain />, document.getElementById('user-list-page'));
}
UsersTable.js
import axios from 'axios';
import React, {useContext,useEffect,useState} from "react";
import { GlobalContext } from "../../state/GlobalContext";
import Preloader from '../Preloader';
import Conf from '../../conf/Conf';
export default function UsersTable(){
const context = useContext(GlobalContext);
const [loading, setLoading] = useState(true);
useEffect(() => {
axios.get('/api/get-all-users')
.then(response => {
context.usersData = response.data.data;
setLoading(false);
})
})
if(loading){
return (
<Preloader isLoading={loading}/>
)
}else{
return (
<>
<Preloader isLoading={loading}/>
<div className="col-12">
<div className="table-responsive rounded-table">
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>Avatar</th>
<th>Name</th>
<th>Surname</th>
<th>Group</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{context.usersData.map((user,index) => {
return (
<tr key={user.email}>
<th>{user.id}</th>
<th>
<div className="avatar" style={{backgroundImage: `url(${Conf().assetPath}uploads/avatars/${user.avatar})`}}></div>
</th>
<th>{user.name}</th>
<th>{user.surname}</th>
<th>Group</th>
<th>{user.email}</th>
<th>
<button type="button" className="btn theme-edit-btn"><i className="fa-solid fa-pencil"></i></button>
<button type="button" className="btn theme-delete-btn"><i className="fa-solid fa-delete-left"></i></button>
</th>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
</>
)
}
}
AddUserButton.js
import React, { useContext } from "react";
import { GlobalContext } from "../../state/GlobalContext";
export default function AddUserButton(){
const context = useContext(GlobalContext);
function triggerUserPopupClick(){
context.clickedNewUserButton = true
}
return(
<div className="col-xl-2 col-md-4 col-12">
<button type="button" onClick={() => {triggerUserPopupClick()}} className="btn theme-primary-btn">Add New <i className="fa-solid fa-angles-right"></i></button>
</div>
)
}
The problem is exactly in the AddUserButton component where I'm trying to update global context to re-render main UsersPageMain component. I don't have any idea what I'm doing wrong... If you could, please give some tips what I need to do to achieve opening popup after clicking this component.
Here's the working snippet on the codesanbox, might it will be helpfull.
https://codesandbox.io/embed/adoring-bell-dv3tfc?fontsize=14&hidenavigation=1&theme=dark
function triggerUserPopupClick(){
context.clickedNewUserButton = true
}
React has only one1 way to cause a rerender: setting state. React can't tell that you mutated the context object here, so it will not rerender.
To make this work, you will need to set up your top level component to have a state variable. It will then make both the state and the setter function available via context.
// In globalcontext.js
export const initialState = {
usersData: null,
clickedNewUserButton: false,
showAddUserPopup: false,
isLoading: true,
};
export const GlobalContext = React.createContext({
state: initialState,
setState: () => {},
});
// In UsersPageMain.js
function UsersPageMain(){
const [state, setState] = useState(initialState);
// It's important to memoize this so you don't make a new object unless
// the state has changed. Otherwise, every render of UsersPageMain will
// necessarily trigger a render of every consumer of the context.
const contextValue = useMemo(() => {
return {
state,
setState,
};
}, [state]);
if(state.clickedNewUserButton){
return (
<GlobalContext.Provider value={contextValue}>
// ...
</GlobalContext.Provider>
);
} else {
return (
<GlobalContext.Provider value={contextValue}>
// ...
</GlobalContext.Provider>
)
}
}
And then you'll call that setState function when you want to change it:
// In AddUserButtonjs
const { state, setState } = useContext(GlobalContext);
function triggerUserPopupClick(){
setState({
...state,
clickedNewUserButton: true
});
}
Don't forget to update any other places where you're using the context so that they expect to get an object with a .state property, instead of getting the state directly.
1) Ok, technically there's a second way: force update in class components. But don't use that
I figured out what was wrong. I wrote the async function and then into the useEffect I resolve the promise. The problem is that still I don't know why my axios was getting into the loop. Here's a working part of code :
import axios from 'axios';
import React, {useContext,useEffect,useState} from "react";
import { GlobalContext } from "../../state/GlobalContext";
import Preloader from '../Preloader';
import Conf from '../../conf/Conf';
async function fetchUsersData() {
const response = await axios.get('/api/get-all-users');
return response;
}
export default function UsersTable(){
const { state, setState } = useContext(GlobalContext);
useEffect( () => {
fetchUsersData().then(response => {
setState({
...state,
usersData: response.data.data,
isLoading: false,
})
})
}, [null]);
return (
<></>
)
}
In the Case One:
Im doing a simple server-side pagination in rails and using react as front-end and redux as my state management. I have done all the things and the last thing remaining is to just pass the new generated url and fetch the new data. This data will be fetched in a another component which will generate and products.
as Im using redux in my case, how am I able to pass this data to my data fetch action ?
In the Case Two:
I have tried passing a parameter named url and dispatching the fetch action again with the url data i give to it. but the return is that the dispatch is not a function. Am I even able to rerun actions in action.jsx ?
action.jsx
export const handlePage = (e, { activePage }) => {
let pageNum = activePage
let pageString = pageNum.toString();
let url = "/api/v1/products/index/?page=" + pageString; ------> Use This ...
}
export const fetchProducts = (url) => { ------> In Here
return (dispatch) => {
console.log(url);
dispatch(fetchProductsRequest());
axios
.get(url)
.then((response) => {
// response.data is the products
const products = response.data.products;
dispatch(fetchProductsSuccess(products));
})
.catch((error) => {
// error.message is the error message
dispatch(fetchProductsFailure(error.message));
});
};
};
export class Paginator extends React.Component {
state = {
page: [],
pages: [],
};
componentDidMount() {
axios
.get("/api/v1/products/index", { withCredentials: true })
.then((response) => {
this.setState({
page: response.data.page,
pages: response.data.pages,
});
})
.catch((error) => {
console.log("Check Login Error", error);
});
}
render() {
return (
<div>
<Pagination count={this.state.pages} page={this.state.page} onChange={handlePage} />
</div>
);
}
}
Product.jsx
import React, { useEffect } from "react";
import { Link } from "react-router-dom";
import "../../style/frequentlyasked.scss";
import ItemOne from "../../files/Item-One.png";
// Redux
import { connect } from "react-redux";
import { loadCurrentItem, addToCart, fetchProducts } from "./action";
const Product = ({
mapProducts,
fetchProducts,
product,
addToCart,
loadCurrentItem,
}) => {
useEffect(() => {
fetchProducts(); -----> Using it Here !
}, []);
return (
<div className="card-deck d-flex justify-content-center">
{mapProducts.map((product) => (
<div className="card item-card" key={product.id} product={product}>
{/* Admin Card */}
{/* Header Image */}
<img className="card-img-top" src={ItemOne} alt="Card image cap" />
{/* Card Body */}
<div className="card-body">
<h4 className="card-title">{product.title}</h4>
<h5 className="card-title">$ {product.price}</h5>
<p className="card-text">{product.description}</p>
<button
className="btn btn-primary"
onClick={() => addToCart(product.id)}
>
+ Add To Cart
</button>
<a href="#" className="btn btn-danger">
<svg
width="1em"
height="1em"
viewBox="0 0 16 16"
className="bi bi-heart-fill"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M8 1.314C12.438-3.248 23.534 4.735 8 15-7.534 4.736 3.562-3.248 8 1.314z"
/>
</svg>
</a>
</div>
{/* Card Footer */}
<div className="card-footer">
<small className="text-muted">Last updated 3 mins ago</small>
</div>
</div>
))}
</div>
);
};
const mapStateToProps = (state) => {
return {
mapProducts: state.shop.products,
};
};
const mapDispatchToProps = (dispatch) => {
return {
addToCart: (id) => dispatch(addToCart(id)),
loadCurrentItem: (item) => dispatch(loadCurrentItem(item)),
fetchProducts: () => dispatch(fetchProducts()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Product);
In your case:
useEffect(() => {
fetchProducts(page); -----> Using it Here !
}, [page]);
//and
fetchProducts: (page) => dispatch(fetchProducts(page)),
Note that your mapDispatchToProps could (and should) also be written in the map object notation:
const mapDispatchToProps = {
addToCart,
loadCurrentItem,
fetchProducts
}
Also note that the official recommendation is to use the react-redux hooks instead of connect and mapDispatchToProps.
So skip the whole connect stuff and in your component:
const Product = ({
product,
}) => {
const mapProducts = useSelector(state => state.shop.products)
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchProducts(page));
}, []);
Also, if you are still using connect, you have probable been following outdated tutorials. Redux has changed a lot over the last few years. Look into modern redux and the up-to-date official redux tutorials
By the way: there is a new API on the way for the official redux toolkit which can take care of all that data fetching for you. You can already try it out, at the moment as an extra package: https://rtk-query-docs.netlify.app/
I am new to React. I searched for the same error of .map is not a function all over. But the solutions provided not worked out for me. In my case, the home screen loads properly initially. But if I redirect to it from another page, the above error occurs.
Here is my code:
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import axios from "axios";
import { useSelector, useDispatch } from "react-redux";
import { listProducts } from "../actions/productActions";
function HomeScreen(props) {
const productList = useSelector((state) => state.productList);
const { products, loading, error } = productList;
const dispatch = useDispatch();
useEffect(() => {
dispatch(listProducts());
return () => {
//
};
}, []);
return loading ? (
<div>Loading....</div>
) : error ? (
<div>{error}</div>
) : (
<ul className="products">
{products.map((product) => (
<li key={product._id}>
<div className="product">
<Link to={"/product/" + product._id}>
<img
className="product-image"
src={product.image}
alt="product"
/>
</Link>
<div className="product-name">
<Link to={"/product/" + product._id}>{product.name}</Link>
</div>
<div className="product-brand">{product.brand}</div>
<div className="product-price">{product.price}</div>
<div className="product-rating">
{product.rating} Stars ({product.numReviews} Reviews)
</div>
</div>
</li>
))}
</ul>
);
}
export default HomeScreen;
While your redux function is being performed, products is undefined,
you theirfore have to wait for data by utilizing your loading parameter to render something untill your store updates or u can try to supplie initial empty array and when data is recived, it will update the DOM like : -
const { products, loading, error } = productList;
const theProducts = products || []
theProducts.map((d) => {})
I had the same problem a few days ago. I found out that in my product list reducer I defined my products as an object instead of an array
export const topProductsReducer = (state = { products: [] }, action) => {
switch (action.type) {
case PRODUCT_TOP_REQUEST:
return { loading: true, products: [] };
case PRODUCT_TOP_SUCCESS:
return { loading: false, products: action.payload, success: true };
case PRODUCT_TOP_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
I had to change the products state from and object to an array as above