I'm building an eCommerce project using hooks for Redux and React and I'm stumped on how to get the individual details for each item. When I click on the item, I only retrieve the details of the first item in the array no matter which item is clicked with the current set up. When I use state.products.find(item => item.id === item) instead of the code below for details const, I get a TypeError: Cannot read property x of undefined. Any help is appreciated.
function App() {
return (
<Provider store={store}>
<Navbar/>
<Switch>
<Route exact path="/" component={Home}/>
<Route exact path='/products' component={Products}/>
<Route exact path="/products/:prodId" component={ProductDetail}/>
<Route exact path="/cart" component={Cart}/>
</Switch>
</Provider>
);
}
Reducer
const initialState = {
items:[],
prodId: {},
count: 0
}
export default function (state = initialState, {payload,type}){
switch(type){
case types.GET_PRODUCTS:
return{
...state,
items: payload,
}
case types.GET_ITEM:
return{
...state,
prodId: payload
}
default:
return state;
}
}
Actions
const api = 'http://localhost:8000/products'
export const getProds = ()=> dispatch =>{
console.log('fethcin')
fetch(api)
.then(res =>res.json())
.then(data => {
// return {type: GET_PRODUCTS, payload:data}
dispatch({type: types.GET_PRODUCTS, payload:data});
}
)
}
export const getDetails = id => dispatch => {
dispatch({type: types.GET_ITEM, payload: id})
}
Product
const Product = ({product}) => {
const dispatch = useDispatch()
const {id, title, price, isFreeShipping} = product
return (
<div className='prod-container'>
<div className="product">
<h2 className='prod-title'>{title}</h2>
<p className='prod-price'>{price}</p>
<span className='prod-ship'>{isFreeShipping}</span>
<button onClick={()=> dispatch(getDetails(id))}><Link to={{pathname:`/products/${id}`}}> View More </Link> </button>
{/* <button className='btn prod-details'>View Item</button> */}
</div>
</div>
)
}
Details
const ProductDetail = ({match}) => {
const product = match.params.prodId
const details = useSelector(state => state.products.items.find(item => item.id === product))
const {sku, count, title, description, availableSizes, price, isFreeShipping} = details
return (
<div className='prodDetaul'>
<p>{title}</p>
<p>{description}</p>
<p>{price}</p>
<p>{isFreeShipping}</p>
<p>{availableSizes}</p>
<p>{sku}</p>
<Counter count={count}/>
<button className='btn btn-chkot'><Link to='/cart'>Cart</Link></button>
</div>
)
}
The problem is with this line:
const details = useSelector(state => state.products.items.find(item => item.id))
What the statement means basically is:
Find the first item in state.products.items that has an id.
You actually need to pass in a parameter, say, the ID of the clicked item. Could be like this:
const details = useSelector(state => state.products.items.find(item => item.id === myItemId))
Since you're using react-router, you can get the product ID with props.match.params.id.
The final solution:
const ProductDetail = ({ match }) => {
const productId = match.params.id
const details = useSelector(state => state.products.items.find(item => item.id === productId))
// ...
}
Related
When I tried to image, image is broken
BASE_URL I want to fetch is composed like this.
{
"renderings": [
{
"_id": "image_file"(string type)
},
...more _id
}
This is a similar topic to the question I posted a few days ago.
But I see the problem seems to arise while implementing the detail page.
// App.tsx
function App() {
return (
<BrowserRouter>
<Route exact path="/">
<Gallery />
</Route>
<Route path="/detail">
<Detail />
</Route>
</BrowserRouter>
);
}
export default App;
// Gallery.tsx
function Gallery() {
const [gallery, setGallery] = useState<any>();
const getGallery = async () => {
const json: GalleriesProps = await (await fetch(BASE_URL)).json();
setGallery(json.renderings);
};
useEffect(() => {
getGallery();
}, []);
return (
<ImgContainer>
<Link to="/detail">
{gallery &&
gallery.map((x: any) => (
<img key={x._id} alt="gallery_logo" src={x._id} />
))}
</Link>
</ImgContainer>
);
}
export default Gallery;
// Detail.tsx
function Detail() {
const [galleryDetail, setGalleryDetail] = useState<any>();
const [clicked, setClicked] = useState(false);
console.log(galleryDetail);
useEffect(() => {
async function fetchGallery() {
const response = await fetch(BASE_URL);
const result: GalleriesProps = await response.json();
setGalleryDetail(result.renderings[0]._id);
}
fetchGallery();
}, []);
const prevPage = () => {
if (clicked) return;
setClicked(true);
};
const nextPage = () => {
if (clicked) return;
setClicked(true);
};
return (
<>
<Container>
<Button onClick={prevPage}>
<FontAwesomeIcon icon={faArrowAltCircleLeft} />
</Button>
<ImageDetail>
{galleryDetail && <img src={galleryDetail} />}
</ImageDetail>
<Button onClick={nextPage}>
<FontAwesomeIcon icon={faArrowAltCircleRight} />
</Button>
</Container>
</>
);
}
export default Detail;
When I click on an image, I go to the detail page, and then I have to implement that image to show,
For example,
If I click the image of the cat.jpeg file, I have to go to the detail page and change the size of cat.jpeg and show the rest as it is,
but I can't quite figure it out.
As said in title, my props is an empty object.
This is my component, in which i want to match a proper object in mapStateToProps.
The matching object exists, because when i pass and x.id === 1 , the object is being rendered.
const UserDetails = ({ episode, history }, props) => {
const { id } = useParams();
// const handleDelete = (id) => {
// if (window.confirm("Are you sure wanted to delete the episode ?")) {
// dispatch(deleteEpisode(id));
// }
// };
console.log("hej", props); // it prints empty object
console.log(episode);
return (
<div>
{episode ? (
<div>
<button onClick={() => history.push("/episodes")}>...back</button>
<h1> Tytuł: {episode.title}</h1>
<h3> Data wydania: {episode.release_date}</h3>
<h3> Series: {episode.series} </h3>
<img src={episode.img} alt="episode" />
{/* <div>
{episode.episode.characters.map((id) => {
const properActor = users.find((el) => el.id == id);
return <div>{`${properActor.name} `}</div>;
})}
</div> */}
<button onClick={() => history.push(`/episode/edit/${episode.id}`)}>
Edit
</button>
{/* <button onClick={() => handleDelete(episode.id)}>Delete</button> */}
<div>
<Link to={`/episodes/${episode.id}/addCharacter`}>
<button type="button">Dodaj postać do: {episode.title}</button>
</Link>
</div>
</div>
) : (
<div>Loading...</div>
)}
</div>
);
};
const mapStateToProps = (state, props) => {
return {
episode: state.episodes.episodes
? state.episodes.episodes.find((x) => x.id === props.match.params.id)
: null,
};
};
export default withRouter(connect(mapStateToProps, null)(UserDetails));
for anyone, who woudl like to see the whole project:
https://stackblitz.com/edit/node-fxxjko?file=db.json
hope it works,
to run the database u have to install npm json-server and run
EDIT:
If i do something like this:
const mapStateToProps = (state, props) => {
console.group("mapStateToProps");
console.log(props); // Does props.match.params.id exist? What is it?
console.log(state.episodes.episodes); // Is there an object in this array whose id matches the above?
console.groupEnd();
return {
episode: state.episodes.episodes
? state.episodes.episodes.find(
(x) => x.episodeId === props.match.params.episodeId
)
: null,
};
};
i see the this result:
https://imgur.com/a/ssrJjHV
You are not properly destructuring the rest of your props. Implement ellipsis to get the rest of the props back
It should be:
const UserDetails = ({ episode, history, ...props}) => {
//rest of your code
}
not:
const UserDetails = ({ episode, history }, props) => {
//rest of your code
}
I'm having trouble accessing the state in one of my components. I have a component where a user adds a name and a weight and then submits it. They are then redirected to the Home Page. What I want to happen is for the name that was inputed to be displayed on the Home Page. I can see the state updating, but I can't figure out how to access the state and have the name show on the Home Page. Any help would be appreciated.
Here is my Home Page component:
const HomePage = () => {
const classes = useStyles();
const name = useSelector(state => state.move.name);
const displayMovementButtons = () => {
if (name) {
return (
<Button
className={classes.movementButtons}
onClick={() => history.push('/movement/:id')}
>
<div className={classes.movementName} >{name}</div>
</Button>
)
};
return <div className={classes.noMovementsMessage} >Click add button to begin</div>
};
return (
<div className={classes.homePageContent} >
<Header title={"Home Page" }/>
<div>{displayMovementButtons()}</div>
<div className={classes.fabDiv}>
<Fab
className={classes.fab}
onClick={() => history.push(`/add`)}>
<AddIcon />
</Fab>
</div>
</div>
);
};
const mapStateToProps = (state) => {
return {
name: state.move.name,
}
};
const withConnect = connect(
mapStateToProps,
);
export default compose(withConnect)(HomePage);
Here is my reducer, where I think the problem is:
const initialState = []
const addMovementReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_MOVEMENT:
return [ ...state, {name: action.name, weight: action.weight} ]
default:
return state;
}
};
export default addMovementReducer;
Here is a screenshot showing the state (note: I added multiple names and weights, I would eventually like each 'name' to appear on the Home Page):
Your move branch of state is an array. You can't access the name by state.move.name. Instead of this you can get an array of movements from redux store and render them with Array.map() method.
const MovementButtons = ({ movements }) => {
return (
<div>
{
movements.map(({ name, weight }) => {
if (name) {
<Button
className={classes.movementButtons}
onClick={() => history.push('/movement/:id')}
key={name}
>
<div className={classes.movementName}>{name}</div>
</Button>
}
return (
<div className={classes.noMovementsMessage}>Click add button to begin</div>
)
})
}
</div>
);
}
const HomePage = () => {
const classes = useStyles();
const movements = useSelector(state => state.move);
return (
<div className={classes.homePageContent} >
<Header title={"Home Page" }/>
<MovementButtons movements={movements} />
<div className={classes.fabDiv}>
<Fab
className={classes.fab}
onClick={() => history.push(`/add`)}>
<AddIcon />
</Fab>
</div>
</div>
);
};
const mapStateToProps = (state) => {
return {
name: state.move.name,
}
};
const withConnect = connect(
mapStateToProps,
);
Each time I open a new post page, the data from the last post shows on the screen for a couple of miliseconds and then the new data is shown.
I have tried to check if the post is loading and set it in the state with the setIsLoading function, but it does not work.
See the code below:
const PostPage = ({post, setPost, isLoading, setIsLoading}) => {
const location = useLocation()
const pathname = location.pathname.split("/")[2]
useEffect(() => {
setIsLoading(true)
const fetchData = async () => {
let res = await axios.get(`http://localhost:4000/posts/${pathname}`)
const postData = res.data
setPost(postData)
setIsLoading(false)
}
fetchData()
}, [])
return (
<>
{!isLoading && (
<div>
<h1>{post.title}</h1>
<p>{post.text}</p>
</div>
)}
</>
)
}
These are my routes:
<Switch>
<Route path="/" exact>
{isLogged && (
<AddPost
posts={posts}
setPosts={setPosts}
postTitle={postTitle}
setPostTitle={setPostTitle}
postText={postText}
setPostText={setPostText}
setIsLoading={setIsLoading}
/>
)}
<div className="posts-row">
{posts.slice(0).reverse().map(post => (
<Post
key={post.id}
id={post.id}
postTitle={post.title}
postText={post.text}
isLogged={isLogged}
posts={posts}
setPosts={setPosts}
/>
))}
</div>
</Route>
<Route path="/posts/:id" exact>
<PostPage
post={post}
setPost={setPost}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
</Route>
</Switch>
In App.js I have isLoading set to true, but it works only for the first post.
For the sake of React please don't report my question :D
Edit:
Post.js
const Post = ({postTitle, postText, isLogged, posts, setPosts, id}) => {
const removePost = async (id) => {
const postArray = posts.filter((post) => (post.id !== id))
let res = await axios.delete(`http://localhost:4000/posts/${id}`)
setPosts(postArray)
}
return (
<div className="post">
<Link to={`/posts/${id}`}>
<strong>
{postTitle}
</strong>
</Link>
<p>{postText}</p>
{isLogged && (
<button onClick={() => removePost(id)}>
🗑️
</button>
)}
</div>
)
}
Your effect hook only runs when the component is mounted for the first time. If you want to run it every time pathname changes, add it to its dependency array:
useEffect(() => {
setIsLoading(true)
const fetchData = async () => {
let res = await axios.get(`http://localhost:4000/posts/${pathname}`)
const postData = res.data
setPost(postData)
setIsLoading(false)
}
fetchData()
}, [pathname])
You need to put setIsLoading inside the fetchData function:
and also add the pathname to useEffect dependencies and also check the pathname to have value:
const fetchData = async () => {
setIsLoading(true);
let res = await axios.get(`http://localhost:4000/posts/${pathname}`);
const postData = res.data;
setPost(postData);
setIsLoading(false);
};
useEffect(() => {
if (pathname) {
fetchData();
}
}, [pathname]);
EDIT:
so here is what you need to do:
import { useParams } from "react-router-dom";
const PostPage = ({ post, setPost, isLoading, setIsLoading }) => {
const { id } = useParams();
const fetchData = async () => {
setIsLoading(true);
let res = await axios.get(`http://localhost:4000/posts/${id}`);
const postData = res.data;
setPost(postData);
setIsLoading(false);
};
useEffect(() => {
if (id) {
fetchData();
}
}, [id]);
return (
<>
{!isLoading && (
<div>
<h1>{post.title}</h1>
<p>{post.text}</p>
</div>
)}
</>
);
};
I have implemented the authentication part of my app (built using the MERN stack). The login action validates login data, then loads user data, then pushes the route to /dashboard. On the dashboard page, I have a simple Welcome to the dashboard, {email}! however I am getting an error telling me that it can not return data of null. I also have the users First & Last name as well as their email in the navbar, that also spawns an error of returning null data. I have a useEffect that loads the user data in my App.js but i'm still receiving the null errors.
Is there a way to load the data prior to render?
Index.js
ReactDOM.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</PersistGate>
</Provider>,
document.getElementById('root')
);
App.js
const App = () => {
const [loading, setLoading] = useState(true);
const dispatch = useDispatch();
useEffect(() => {
// check for token in LS
if (localStorage.token) {
setAuthToken(localStorage.token);
}
dispatch(attemptGetUser())
.then(() => setLoading(false))
.catch(() => setLoading(false));
// Logout user from all tabs if they logout in another tab
window.addEventListener('storage', () => {
if (!localStorage.token) dispatch({ type: LOGOUT });
});
// eslint-disable-next-line
}, []);
return loading ? (
<Loading cover="page" />
) : (
<div className="App">
<Switch>
<Route path="/" component={Views} />
</Switch>
</div>
);
};
redux/thunks/Auth.js
export const attemptLogin = (formData) => async (dispatch) => {
await postLogin(formData)
.then((res) => {
dispatch(login(res.data));
dispatch(push('/dashboard'));
})
.then(() => {
dispatch(attemptGetUser());
})
.catch((error) => {
const errors = error.response.data.message;
dispatch(setAlert('Uh-oh!', errors, 'error'));
});
};
redux/thunks/User.js
export const attemptGetUser = () => async (dispatch) => {
await getUser()
.then((res) => {
dispatch(setUser(res.data));
})
.catch((error) => {
const errors = error.response.data.message;
console.log(errors);
dispatch(setAlert('Uh-oh!', errors, 'danger'));
});
};
views/app-views/dashboard
const Dashboard = () => {
const { email } = useSelector((state) => state.user.user);
return (
<div>
Welcome to the dashboard,
<strong>{email}</strong>!
</div>
);
};
export default Dashboard;
components/layout-components/NavProfile.js
export const NavProfile = () => {
const { firstName, lastName, email } = useSelector(
(state) => state.user.user
);
const dispatch = useDispatch();
const onLogout = () => {
dispatch(attemptLogout());
};
const profileImg = '/img/avatars/thumb-1.jpg';
const profileMenu = (
<div className="nav-profile nav-dropdown">
<div className="nav-profile-header">
<div className="d-flex">
<Avatar size={45} src={profileImg} />
<div className="pl-3">
<h4 className="mb-0">{firstName} {lastName}</h4>
<span className="text-muted">{email}</span>
</div>
</div>
</div>
<div className="nav-profile-body">
<Menu>
{menuItem.map((el, i) => {
return (
<Menu.Item key={i}>
<a href={el.path}>
<Icon className="mr-3" type={el.icon} />
<span className="font-weight-normal">{el.title}</span>
</a>
</Menu.Item>
);
})}
<Menu.Item key={menuItem.legth + 1} onClick={onLogout}>
<span>
<LogoutOutlined className="mr-3" />
<span className="font-weight-normal">Logout</span>
</span>
</Menu.Item>
</Menu>
</div>
</div>
);
return (
<Dropdown placement="bottomRight" overlay={profileMenu} trigger={['click']}>
<Menu className="d-flex align-item-center" mode="horizontal">
<Menu.Item>
<Avatar src={profileImg} />
</Menu.Item>
</Menu>
</Dropdown>
);
};
export default NavProfile;
So the error is telling you that in your redux state that state.user.user is undefined, this is why you can't destructure firstName, lastName, email values.
If in your store state.user.user is at least a defined, empty object ({}) then the access of null errors should resolve.
const userReducer = (state = { user: {} }, action) => {
...
}
This can still potentially leave your UI rendering "undefined", so in the component code you'll want to provide default values, i.e.
const { firstName = '', lastName = '', email = '' } = useSelector(
(state) => state.user.user
);
The alternative is to have fully qualified initial state in your user reducer slice.
const initialState = {
user: {
firstName: '',
lastName: '',
email: '',
},
};
const userReducer = (state = initialState, action) => {
...
}
Seems like you could fix this by changing your Redux store initial state.
Taking your Dashboard component as an example:
const Dashboard = () => {
const { email } = useSelector((state) => state.user.user);
return (
<div>
Welcome to the dashboard,
<strong>{email}</strong>!
</div>
);
};
It expects that there is a user object with an email string in the user slice of state in your Redux store. As noted in their documentation
You could update your createStore call to include a initial value for the redux store like createStore({'user': {'user': {'email': ''}}}); for example