Help! My child component is not updating in my react app!
I want to bring cartNumber to the page component which then is passed onto header component but the number doesn't even show up!
Parent component
class Shop extends Component {
constructor(props) {
super(props);
this.state = {
merchants: [],
error: null,
loading: true,
order: []
};
}
componentWillMount() {
Meteor.call("merchants.getMerchants", (error, response) => {
if (error) {
this.setState(() => ({ error: error }));
} else {
this.setState(() => ({ merchants: response }));
}
});
}
componentDidMount() {
setTimeout(() => this.setState({ loading: false }), 800); // simulates loading of data
}
goBack = () => this.props.history.push("/");
goCart = () => {
try {
Orders.insert(this.state.order), this.props.history.push("/cart");
} catch (error) {
throw new Meteor.Error("there was an error", error);
}
};
onAddToCart(cartItem) {
let { order } = this.state;
order.push(cartItem);
console.log(order.length);
}
render() {
const { loading } = this.state;
const { merchants, error } = this.state;
const { data } = this.state;
const { order } = this.state;
const getProductsFromMerchant = ({ products, brands }) =>
products.map(({ belongsToBrand, ...product }) => ({
...product,
brand: brands[belongsToBrand]
}));
const products = merchants.reduce(
(acc, merchant) => [...acc, ...getProductsFromMerchant(merchant)],
[]
);
if (loading) {
return (
<Page
pageTitle="Shop"
history
goBack={this.goBack}
goCart={this.goCart}
>
<div className="loading-page">
<i
className="fa fa-spinner fa-spin fa-3x fa-fw"
aria-hidden="true"
/>
<br /> <br />
<span>Loading...</span>
</div>
</Page>
);
}
return (
<Page
pageTitle="Shop"
history
goBack={this.goBack}
goCart={this.goCart}
cartNumber={order.length}
>
<div className="shop-page">
{products.map(({ id, ...product }) =>
<Product
{...product}
key={id}
history
onAddToCart={this.onAddToCart.bind(this)}
/>
)}
</div>
</Page>
);
}
}
export default Shop;
Here is the page component which contains the header component
export const Page = ({
children,
pageTitle,
history,
goBack,
goCart,
cartNumber
}) =>
<div className="page">
<Header goBack={goBack} goCart={goCart} history cartNumber>
{pageTitle}
</Header>
<main>
<MuiThemeProvider>
{children}
</MuiThemeProvider>
</main>
<Footer />
</div>;
export default Page;
And Finally this is the header where I want to bring the cartNumber into.
const Header = ({ children, goBack, goCart, cartNumber, pageTitle }) =>
<header>
<button onClick={goBack} className="back-button">
{/* Image added here to show image inclusion, prefer inline-SVG. */}
<img alt="Back" src={`/icon/header/back-white.svg`} />
</button>
<h1>
{children}
</h1>
<div className="right-content">
( {cartNumber} )
<i
className="fa fa-shopping-cart fa-2x"
aria-hidden="true"
onClick={goCart}
/>
</div>
</header>;
export default withRouter(Header);
You're passing cartNumber as a boolean:
<Header goBack={goBack} goCart={goCart} history cartNumber>
Pass it as a value:
<Header goBack={goBack} goCart={goCart} history={history} cartNumber={cartNumber}>
Related
NewsDetails
import React, { useState, useEffect } from 'react'
import { useParams } from 'react-router-dom'
const NewsDetail = ({ state }) => {
const { id } = useParams();
return (
<div>
{
state
.filter((a) => a.id === id)
.map((card, index) => (
<>
<div className="card" key={index}>
<h2>{card.title}</h2>
<h2>{card.content}</h2>
<img src={card.imageUrl} alt="" />
</div>
</>
))
}
</div>
)
}
export default NewsDetail
NewsItem
import React from 'react'
import clock from "../components/assets/img/Clock.svg"
import user from "../components/assets/img/User.svg"
import { Link } from 'react-router-dom'
const NewsItem = (props) => {
const { imageUrl, title, author, content, date, id } = props
return (
<Link className="col-lg-4 p-2" to={`/detail/${id}`}>
<div className="newsItem">
<img src={imageUrl} alt='newsPhoto' />
<h2>{id}</h2>
<div className="itemBody">
<p className='title'>{title}</p>
<div className="line"></div>
<p className='content'>{content}</p>
<div className="itemfooter">
<span><img src={clock} alt='clock' />{date}</span>
<span><img src={user} alt='user' />{author}</span>
</div>
</div>
</div>
</Link>
)
}
export default NewsItem
Home
import React, { useEffect, useState } from "react";
import NewsItem from "./NewsItem";
import SpinnerLoad from "./SpinnerLoad";
import { v4 as uuidv4 } from 'uuid';
const Home = (props) => {
const Category = [
"all",
"business",
"sports",
"world",
"technology",
"entertainment",
"science"
];
const { state, setState} = props;
const [loading, setLoading] = useState(false)
const fetchValue = (category) => {
fetch(`https://inshorts-api.herokuapp.com/news?category=${category}`)
.then(res => res.json())
.then(res => {
setState(res.data)
setLoading(true)
})
.catch((error) => console.log(error))
console.log(state);
setLoading(false);
};
// const fetchValue = async () => {
// try {
// const data = await axios
// .get(`https://inshorts-api.herokuapp.com/news?category=sports`)
// .then(res => {
// console.log(res);
// setState(res.data)
// })
// setLoading(true)
// console.log(loading);
// } catch (e) {
// console.log(e);
// }
// }
const CategoryButton = ({ category }) => (
<button onClick={() => fetchValue(category)} style={{ textTransform: 'capitalize' }}>{category}</button>
);
useEffect(() => {
fetchValue('all')
},[])
return (
<>
<div className="header-bg">
<h1 className="mb-3">News</h1>
<div className="btns ">
{Category.map((value, index) => {
return <CategoryButton category={value} key={index} />;
})}
</div>
</div>
<div className="news">
<div className="container">
<div className="row">
{
!loading
? <SpinnerLoad/>
:
state.map((data,index) => {
return (
<NewsItem
imageUrl={data.imageUrl}
author={data.author}
title={data.title}
content={data.content}
date={data.date}
id={uuidv4()}
key={index}
/>
);
})
}
</div>
</div>
</div>
</>
);
};
export default Home;
I have created a project with api. With categories it is possible to change the incoming data, but there is one thing where I want to get more detailed information when I click on the newsItem card. That api doesn't have id value, so I used uuid. Information corresponding to the id value should come with useParams. But it doesn't work. How can I fix this problem?
The first issue is that you are generating a GUID when rendering the state array which won't necessarily correlate to any data you are trying to match/filter by in the NewsDetail component.
state.map((data,index) => (
<NewsItem
imageUrl={data.imageUrl}
author={data.author}
title={data.title}
content={data.content}
date={data.date}
id={uuidv4()} // <-- new id each render cycle
key={index}
/>
))
You want to inject the id property when the data is fetch so that it's a stable reference that lives as long as the data does. In other words, it should be an intrinsic property of the data.
Example:
const fetchValue = async (category) => {
setLoading(true);
try {
const res = await fetch(`https://inshorts-api.herokuapp.com/news?category=${category}`);
const { data } = await res.json();
setState(data.map(el => ({
...el,
id: uuidv4(), // <-- map and inject id here
})));
} catch(error) {
console.log(error);
} finally {
setLoading(false);
}
};
...
state.map((data) => (
<NewsItem
key={data.id} // <-- use as React key
data={data} // <-- pass entire data object as prop
/>
))
NewsItem
const NewsItem = ({ data }) => {
const { imageUrl, title, author, content, date, id } = data;
return (
...
);
};
NewsDetail
const NewsDetail = ({ state }) => {
const { id } = useParams();
return (
<div>
{state
.filter((card) => card.id === id)
.map((card) => (
<div className="card" key={card.id}>
<h2>{card.title}</h2>
<h2>{card.content}</h2>
<img src={card.imageUrl} alt="" />
</div>
))
}
</div>
);
};
I have a problem with my react app. I have a blog page where I can create blog posts and display them to the screen. In this part everything works fine. User logs in and can write a post. Each post contains a Read more... link and if the user clicks on that link the app redirects to the actual blog post. There the user can read the whole blog and add some comments. Everything works perfectly except when the user refreshes the page, everything disappears without any error in the console. I use firebase as my back-end and everything is saved there just like it has to be. Each time I click on the particular post I get redirected to that post and everything is ok, but when I refresh the page everything disappears, the post, the comments, even the input field and the submit comment button.
Here is a picture before refresh:
Before
here is a picture after refresh:
After
Also I will include the code for the actual blog and comment section.
The BlogAndCommentPage contains the actual blog post and holds the input field for the comments and the comments that belong to this post.
import React from 'react'
import { projectFirestore } from '../../firebase/config';
import BackToBlogs from './BackToBlogs'
import AddComment from '../commentComponents/AddComment'
class BlogAndCommentPage extends React.Component {
state = { param: '', blog: [] }
componentDidMount = () => {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString)
const id = urlParams.get('id')
this.setState({ param: id })
const fetchDataFromFireBase = async () => {
projectFirestore.collection('Blogs').doc(id).get()
.then(doc => {
if(doc.exists) {
let document = [];
document.push(doc.data());
this.setState({ blog: document })
}
})
}
fetchDataFromFireBase()
}
renderContent() {
// Display the blog
const blogs = this.state.blog?.map(value => {
return (
<div key={value.post.blogID}>
<h1>{value.post.title}</h1>
<h6>{`${value.post.name} - ${value.post.date}`}</h6>
<p>{value.post.body}</p>
</div>
)
})
return blogs;
}
render() {
const displayedBlog = this.state.param
return (
<div>
{
displayedBlog ? (
<div>
{this.renderContent()}
<BackToBlogs />
<hr></hr>
<h5 className="mb-2">Add a comment</h5>
<AddComment param={this.state.param} />
</div>
) : ''
}
</div>
)
}
}
export default BlogAndCommentPage
The AddComment component holds the submit button for the comments and the list of the components
import React, { useState, useEffect } from 'react'
import SubmitComment from './SubmitComment'
import CommentHolder from './CommentHolder';
import { useSelector, useDispatch } from 'react-redux';
const AddComment = ({ param }) => {
const [comment, setComment] = useState('');
useEffect(() => {
if (sessionStorage.getItem('user') === null) {
alert('You are not logged in. Click OK to log in.')
window.location = 'http://localhost:3000/'
}
}, [])
const dispatch = useDispatch();
const state = useSelector((state) => state.state);
if (state) {
setTimeout(() => {
setComment('')
dispatch({ type: "SET_FALSE" })
}, 50)
}
return (
<div>
<div>
<div className="row">
<div className="col-sm">
<div className="form-group">
<textarea rows="4" cols="50" placeholder="Comment" className="form-control mb-3" value={comment} onChange={(e) => setComment(e.target.value)} />
</div>
</div>
</div>
</div>
<div className="mb-3">
<SubmitComment comment={comment} param={param} />
</div>
<CommentHolder param={param} />
</div>
)
}
export default AddComment
The CommentHolder renders each comment that belong to that post
import React from 'react';
import { projectFirestore } from '../../firebase/config';
import DeleteComment from './DeleteComment'
class CommentHolder extends React.Component {
state = { docs: [] }
_isMounted = false;
componentDidMount = () => {
const fetchDataFromFireBase = async () => {
const getData = await projectFirestore.collection("Comments")
getData.onSnapshot((querySnapshot) => {
var documents = [];
querySnapshot.forEach((doc) => {
documents.push({ ...doc.data(), id: doc.id });
});
if (this._isMounted) {
this.setState({ docs: documents })
}
});
}
fetchDataFromFireBase()
this._isMounted = true;
}
componentWillUnmount = () => {
this._isMounted = false;
}
renderContent() {
// Delete comments
const deleteComment = async (id) => {
projectFirestore.collection('Comments').doc(String(id)).delete().then(() => {
console.log(`Blog with id: ${id} has been successfully deleted!`)
})
}
// Build comments
let user;
if (sessionStorage.getItem('user') === null) {
user = [];
} else {
user = JSON.parse(sessionStorage.getItem('user'));
const commentArray = this.state.docs?.filter(value => value.blogID === this.props.param)
.sort((a, b) => (a.time > b.time) ? -1 : (b.time > a.time) ? 1 : 0)
.map(comment => {
return (
<div key={comment.id} className="card mb-3" >
<div className="card-body">
<div className="row">
<div className="col-sm">
<h6>{`${comment.name} - ${comment.time}`}</h6>
<p>{comment.comment}</p>
</div>
<div className="col-sm text-right">
{user[0].id === comment.userID ? <DeleteComment commentid={comment.id} onDeleteComment={deleteComment} /> : ''}
</div>
</div>
</div>
</div>
)
});
const updateComments = () => {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString)
const id = urlParams.get('id')
const updateComment = projectFirestore.collection('Blogs').doc(id);
return updateComment.update({
'post.comments': commentArray.length
})
}
updateComments()
return commentArray;
}
}
render() {
return (
<div>
{this.renderContent()}
</div>
)
}
}
export default CommentHolder
The DeleteComment deletes the comment
import React from 'react'
const DeleteComment = ({ commentid, onDeleteComment }) => {
return (
<div>
<button onClick={() => onDeleteComment(commentid)} className='btn btn-outline-danger'>X</button>
</div>
)
}
export default DeleteComment
The SubmitComment stores the comment in the Firebase
import React from 'react'
import { projectFirestore } from '../../firebase/config';
import { v4 as uuidv4 } from 'uuid';
import { useDispatch } from 'react-redux';
const SubmitComment = ({ comment, param }) => {
const dispatch = useDispatch();
const onCommentSubmit = () => {
let user;
if (sessionStorage.getItem('user') === null) {
user = [];
} else {
user = JSON.parse(sessionStorage.getItem('user'));
projectFirestore.collection('Comments').doc().set({
id: uuidv4(),
comment,
name: `${user[0].firstName} ${user[0].lastName}`,
userID: user[0].id,
blogID: param,
time: new Date().toLocaleString()
})
dispatch({ type: "SET_TRUE" });
}
}
return (
<div>
<button onClick={() => onCommentSubmit()} className='btn btn-primary'>Add comment</button>
</div>
)
}
export default SubmitComment
In case there is a rout problem here is the code for the routing between the blogs section and the blog + comments section
return (
<Router >
<Route path='/content-page' exact render={(props) => (
<>
<BlogAndCommentPage />
</>
)} />
<Route path='/blogpage' exact render={(props) => (
<>
<div>
<div className="row">
<div className="col-8">
<h1 className='mb-3'>Blog</h1>
</div>
<div className="col-4 mb-3">
<LogoutButton onLogOut={logout} />
<h6 className='float-right mt-4 mr-2'>{displayUser}</h6>
</div>
</div>
{empty ? (<div style={{ color: "red", backgroundColor: "#F39189", borderColor: "red", borderStyle: "solid", borderRadius: "5px", textAlign: 'center' }} className="mb-2">Title and body cannot be blank</div>
) : ("")}
<InputArea getBlogContent={getBlogContent} />
<CreateBlog post={post} onCheckEmptyInputs={checkEmptyTitleAndBody} />
<hr />
<BlogHolder />
</div>
</>
)} />
</Router>
)
If anybody has any clue on why is this happening, please let me know.
Thank you.
As your website is CSR (client side rendering) it doesn't understand the URL in the first execution, you might need to configure a hash router, take a look at:
https://reactrouter.com/web/api/HashRouter
Also, there is a good answer about it here
Thanks so much for your help in advance. My application where users can select some videos and then saved works in React + Youtube API. I've been successfully fetched videos from Youtube right now. But I have two matters for completely building it done.
1) onClick() is not working in CardAction Container
2) isSelected is not working in Youtube.js
src/components/common/Youtube.js
const Youtube = ({ video, handleSelect, classes, selectedVideos }) => {
const url = "https://www.youtube.com/embed/" + video.id.videoId;
const videoInfo = video.snippet;
const isSelected = selectedVideos.find(v => v.uid === video.id.videoId);
return (
<div>
{isSelected ? (
<Card className={classes.card}>
<iframe
id="ytplayer"
type="ytplayer"
width="100%"
height="400"
src={url}
frameborder="0"
/>
<CardActionArea
onClick={handleSelect(video.id.videoId)}
>
<h2>{videoInfo.title}</h2>
<p>{videoInfo.description}</p>
</CardActionArea>
</Card>
) : (
<Card className={classes.card}>
<iframe
id="ytplayer"
type="ytplayer"
width="100%"
height="270"
src={url}
frameborder="0"
/>
<CardActionArea
onClick={handleSelect(video.id.videoId)}
>
<h2>{videoInfo.title}</h2>
<p>{videoInfo.description}</p>
</CardActionArea>
</Card>
)}
</div>
);
};
export default Youtube;
src/components/common/YoutubeList.js
const YoutubeList = ({ videos, handleSelect, selectedVideos }) =>
videos.map(video => (
<Youtube
video={video}
handleSelect={handleSelect}
selectedVideos={selectedVideos}
/>
));
export default YoutubeList;
src/components/Home.js
const YOUTUBE_API_KEY = process.env.REACT_APP_YOUTUBE_API_KEY;
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
videos: [],
selectedVideos: []
};
}
onSerchYoutube = keyword => {
const url = `https://www.googleapis.com/youtube/v3/search?type=video&part=snippet&q=${keyword}&maxResults=3&key=${YOUTUBE_API_KEY}`;
axios
.get(url)
.then(response => {
this.setState({
videos: response.data.items
});
})
.catch(() => {
console.log("Failed to fetch videos :(");
});
};
_handleSelect = id => {
const { selectedVideos } = this.state;
selectedVideos.push({ uid: id });
console.log("selected!" + id);
console.log("selected", selectedVideos);
};
render() {
return (
<div>
<div>
<Header title={"Home"} />
<SearchForm onSerchYoutube={this.onSerchYoutube} />
<YoutubeList
videos={this.state.videos}
handleSelect={this._handleSelect}
selectedVideos={this.state.selectedVideos}
/>
</div>
</div>
);
}
}
export default Home;
Can you use ES6?
change
onClick={handleSelect(video.id.videoId)}
to
onClick={() => handleSelect(video.id.videoId)}
I am having a problem related to redux.
I have 2 connected components which are:
avatar situated in the navbar which is always visible
profile which is responsible for changing the avatar image in the store
if I am right, when the store change, any connected component will re-render if needed.
In my case, when the action UPDATE_CURRENT_USER update the avatar image, the navbar avatar doesn't get the new image only after I change route or reload page.
I found a solution but many people say it's a hack,
I have put a listener on store changes in the main component and did forceUpdate()
componentDidMount() {
store.subscribe(res => this.forceUpdate());
}
and I don't want to use it since connected components are supposed to re-render on store changes.
user actions:
export const getCurrentUser = () => dispatch => {
axios.get("user").then(user => {
dispatch({
type: GET_CURRENT_USER,
payload: user.data
});
});
};
export const updateCurrentUser = user => dispatch => {
dispatch({
type: UPDATE_CURRENT_USER,
payload: user
})
}
user reducer
const initialState = {
user: {}
}
export default function (state = initialState, action) {
switch (action.type) {
case GET_CURRENT_USER:
return { ...state, user: action.payload };
case UPDATE_CURRENT_USER:
return { ...state, user: action.payload }
default:
return state;
}
}
profile component
class Profile extends Component {
render() {
const { currentUser, updateCurrentUser } = this.props;
return (
<div id="profile-container">
<ProfileSider
currentUser={currentUser}
updateCurrentUser={updateCurrentUser}
/>
<ProfileContent
currentUser={currentUser}
updateCurrentUser={updateCurrentUser}
/>
</div>
);
}
}
const mapStateToProps = state => ({
currentUser: state.userReducer.user
});
export default connect(
mapStateToProps,
{ updateCurrentUser }
)(Profile);
profile sidebar child of profile
class ProfileSider extends Component {
state = { uploading: false };
triggerAvatarInput() {
$("#avatarInput").click();
}
handleChange = async event => {
this.setState({ ...this.state, uploading: true });
const avatarFormData = new FormData();
avatarFormData.append("file", event.target.files[0]);
axios
.post("uploadFile", avatarFormData)
.then(res => {
const avatarURIFormData = new FormData();
avatarURIFormData.append("avatar", res.data.fileDownloadUri);
axios
.put("user/update", avatarURIFormData)
.then(res => {
const { currentUser } = this.props;
currentUser.avatar = res.data.avatar;
this.props.updateCurrentUser(currentUser);
this.setState({
...this.state,
uploading: false,
avatar: currentUser.avatar
});
message.success("Avatar updated successfully", 3);
})
.catch(error => {
this.setState({ ...this.state, uploading: false });
message.error("Updating avatar failed!", 3);
});
})
.catch(error => {
this.setState({ ...this.state, uploading: false });
message.error("Uploading avatar failed!", 3);
});
};
render() {
const { uploading } = this.state;
const { currentUser } = this.props;
return (
<div id="profile-sider">
<div id="profile-sider-info">
<div id="profile-sider-info-avatar">
<div className="container">
<div
className="overlay-uploading"
className={
uploading ? "overlay-uploading" : "overlay-uploading hidden"
}
>
<Icon type="loading" style={{ fontSize: 50, color: "#FFF" }} />
</div>
<div className="overlay" />
<div className="overlay-text" onClick={this.triggerAvatarInput}>
<Icon type="camera" style={{ fontSize: 20 }} />
<span>Update</span>
</div>
<div
className="avatar"
style={{
backgroundImage: "url(" + currentUser.avatar + ")"
}}
></div>
<input
onChange={this.handleChange}
type="file"
accept="image/png, image/jpeg, image/jpg"
id="avatarInput"
/>
</div>
</div>
<h2 style={{ marginTop: 20, textAlign: "center" }}>
{currentUser.fullName}
</h2>
<h4 style={{ textAlign: "center" }}>{currentUser.email}</h4>
</div>
<div id="profile-sider-actions">
<div className="profile-sider-actions-item">
<Link to="/profile/courses" style={{ transition: 0 }}>
<Button type="primary" id="courses-btn">
<Icon type="read" style={{ marginRight: 15 }} />
My Courses
</Button>
</Link>
</div>
<div className="profile-sider-actions-item">
<Link to="/profile/update">
<Button type="primary" id="update-infos-btn">
<Icon type="sync" style={{ marginRight: 15 }} />
Update Infos
</Button>
</Link>
</div>
</div>
</div>
);
}
}
export default ProfileSider;
avatar component situated in navbar
class ProfileAvatar extends Component {
constructor() {
super();
this.handleClick = this.handleClick.bind(this);
this.handleOutsideClick = this.handleOutsideClick.bind(this);
this.state = {
showProfileDropdown: false
};
}
componentDidMount() {
this.props.getCurrentUser();
}
handleLogout = async () => {
try {
await auth.logout();
this.props.onLogout();
notification["success"]({
message: "You have been successfully logged out!"
});
} catch (ex) {}
};
handleClick() {
if (!this.state.showProfileDropdown) {
// attach/remove event handler
document.addEventListener("click", this.handleOutsideClick, false);
} else {
document.removeEventListener("click", this.handleOutsideClick, false);
}
this.setState(prevState => ({
showProfileDropdown: !prevState.showProfileDropdown
}));
}
handleOutsideClick(e) {
// ignore clicks on the component itself
if (this.element && this.element.contains(e.target)) {
return;
}
this.handleClick();
}
render() {
const { currentUser } = this.props;
return (
<div
className="profile-avatar"
ref={element => {
this.element = element;
}}
>
<Avatar
onClick={this.handleClick}
size="large"
style={{ color: "#f56a00", backgroundColor: "#fde3cf" }}
src={currentUser.avatar}
>
{currentUser.fullName ? currentUser.fullName.charAt(0) : null}
</Avatar>
{this.state.showProfileDropdown && (
<div className="profile-dropdown-list">
<List
className="dropdown_list dropdown-shadow "
size="small"
style={{ width: "150px" }}
bordered
itemLayout="vertical"
dataSource={[
<Link to="/profile/update" className="profile-list-item">
<List.Item className="list-item">
<Icon className="profile-icons" type="user" /> My Profile
</List.Item>
</Link>,
<Link to="/profile/courses" className="profile-list-item">
<List.Item className="list-item">
<Icon className="profile-icons" type="container" /> My
Courses
</List.Item>
</Link>,
<List.Item className="list-item">
<Icon className="profile-icons" type="question-circle" /> Ask
for Help
</List.Item>,
<List.Item className="list-item" onClick={this.handleLogout}>
<Icon className="profile-icons" type="logout" /> Log out
</List.Item>
]}
renderItem={item => item}
/>
</div>
)}
</div>
);
}
}
const mapStateToProps = state => ({
currentUser: state.userReducer.user
});
export default connect(
mapStateToProps,
{ getCurrentUser }
)(ProfileAvatar);
image: https://imge.to/i/vywTNj
There are two problems here:
You are mutating the existing object from the store
You are sending that exact same user object back into the store when you dispatch the action.
Specifically, these lines are the cause:
const { currentUser } = this.props;
currentUser.avatar = res.data.avatar;
this.props.updateCurrentUser(currentUser);
currentUser is the user object that's already in the Redux store. This code mutates the object, and inserts it back into the store.
That results in the connected component thinking nothing has actually changed.
The shortest way to fix this is to create a new user object, and insert that:
const {currentUser} = this.props;
const updatedUser = {...currentUser, avatar: res.data.avatar};
this.props.updateCurrentUser(updatedUser);
To avoid this in the future, I strongly encourage you to use the configureStore function from our Redux Starter Kit package, which detects mutations and will throw errors if you mutate.
Image of the consoleI'm trying to create a website using the movie DB API. I've created a carousel with some movies and want to open a new page with comprehensive information about a movie by clicking on the movie poster. I'm using componentDidMount to fetch data in one component to create a UI and I pass a movie ID to another component where I use componentWillReceiveProps to fetch another data by using the ID. It worked until I started using state, so now it shows two arrays in the console before I click on a movie poster and when I click on a poster it shows an array and loads a movie data from API then if I click on another poster it loads two different objects with the previous and current movie. I found out that componentWillReceiveProps is dangerous to use but componentDidUpdate works in the same manner.
The main idea is when a user clicks on a poster it gets its id and sends it to another component where the id goes to a link
with complete information about the movie. Are there any patterns to achieve it?
class Data extends Component {
state = {
movies: [],
movieId: null
};
onClick = e => {
this.setState({
movieId: e.target.id
});
console.log(e.target.id);
};
componentDidMount() {
fetch(url)
.then(res => res.json())
.then(data => {
let movies = data.results.map(item => {
return (
<Link to="/movieInfo">
<div className="overlay" onClick={this.onClick}>
<img
src=
{`https://image.tmdb.org/t/p/w500/${item.poster_path}`}
alt={item.title}
id={item.id}
/>
</div>
</Link>
);
});
this.setState({
movies: movies
});
})
.catch(err => console.log(err));
}
render() {
const { movies, movieId } = this.state;
return (
<div className="carousel">
<Slider movie={movies} />
<div className="notShow">
<AdditionalInfo id={movieId} />
</div>
</div>
);
}
}
class AdditionalInfo extends Component {
state = {
movie: []
};
componentDidUpdate(prevProps) {
if (prevProps.id !== null && prevProps.id !== this.props.id) {
fetch(
`https://api.themoviedb.org/3/movie/${
prevProps.id
}?api_key=81f382d33088c6d52099a62eab51d967&language=en-US`
)
.then(res => res.json())
.then(data =>
this.setState({
movie: data
})
);
} else {
return null;
}
}
render() {
const { movie } = this.state;
console.log(movie);
return (
<div className="movieInfo-container">
{/* <section className="title" />
<section className="cast">{movie.id}</section> */}
work
</div>
);
}
}
let movieArr = [];
class Slider extends Component {
state = {
currentIndex: 0,
translateValue: 0
};
createNestedArr = () => {
while (this.props.movie.length) {
movieArr.push(this.props.movie.splice(0, 5));
}
return movieArr.map((item, i) => {
return <Slide key={i} movieGroup={item} />;
});
};
nextPic = () => {
if (this.state.currentIndex === movieArr.length - 1) {
return this.setState({
currentIndex: 0,
translateValue: 0
});
}
this.setState(prevState => ({
currentIndex: prevState.currentIndex + 1,
translateValue: prevState.translateValue - this.slideWidth()
}));
};
prevPic = () => {
if (this.state.currentIndex === movieArr.length + 1) {
return this.setState({
currentIndex: 0,
translateValue: 0
});
} else if (this.state.currentIndex === 0) {
return this.setState({
currentIndex: 0,
translateValue: 0
});
}
this.setState(prevState => ({
currentIndex: prevState.currentIndex - 1,
translateValue: prevState.translateValue + this.slideWidth()
}));
};
slideWidth = () => {
return document.querySelector(".new-releases-slide").clientWidth;
};
render() {
return (
<React.Fragment>
<div
className="movie-carousel"
style={{
transform: `translateX(${this.state.translateValue}px)`,
transition: "transform ease-out 0.45s"
}}
>
{this.createNestedArr()}
</div>
<LeftArrow prevPic={this.prevPic} />
<RightArrow nextPic={this.nextPic} />
</React.Fragment>
);
}
}
const Slide = props => {
const { movieGroup } = props;
return <div className="new-releases-slide">{movieGroup}</div>;
};
Use componentDidMount in your AdditionalIno component. You need to pass the id of the clicked movie to MovieInfo component. This <Link to="/movieInfo"> needs to <Link to={'/movieInfo/${item.id}'}> and in your MovieInfo component access the id using const { id } = this.props.match.params;.
import React, { Component } from 'react';
import Loader from "react-loader-spinner";
class AdditionalInfo extends Component {
state = {
movie: [],
isLoading: true,
};
componentDidMount = () => {
const { id } = this.props;
if (!id) {
return;
}
fetch(
`https://api.themoviedb.org/3/movie/${id}?api_key=81f382d33088c6d52099a62eab51d967&language=en-US`
)
.then(res => res.json())
.then(data =>
this.setState({
movie: data,
isLoading: false,
})
);
}
render() {
const { movie } = this.state;
return (
<div className="movieInfo-container">
{this.state.isLoading
? <Loader type="Puff" color="#00BFFF" height="100" width="100" />
: <div><section className="title" />
<h1>{movie.title}</h1>
<section className="cast">ID: {movie.id}</section>
<h2>Overview</h2>
<p>{movie.overview}</p></div>
}
</div>
);
}
}
export default AdditionalInfo;
then in your Data component change your componentDidMount
componentDidMount = () => {
fetch(url)
.then(res => res.json())
.then(data => {
let movies = data.results.map(item => {
return (
<Link to={`/movieInfo/${item.id}`}>
<div className="overlay" onClick={this.onClick}>
<img
src=
{`https://image.tmdb.org/t/p/w500/${item.poster_path}`}
alt={item.title}
id={item.id}
/>
</div>
</Link>
);
});
this.setState({
movies: movies
});
})
.catch(err => console.log(err));
}
In your MovieInfo do something like
class MovieInfo extends Component {
render() {
const {id} = this.props.match.params;
return (
<div>
<AdditionalInfo id={id} />
</div>
)
}
}
Your router should be like
<Route path="/movieInfo/:id" exact component={MovieInfo} />
Working Demo