I have a User page component which has a link that is meant to open a FollowingModal modal component to show the followers of a user.
The User component calls an async function in my followers context to retieve the user's details of the user page I am on and uses the data to update a userDetails state hook.
I'm then attempting to pass the data in the state object to mt FollowingModal as props but the modal always shows these props as undefined.
I presume this is something like the modal is being rendered before the state is updated, but I'm not sure how to go about this in order to get the desired result of these props being properly initialized and passed to the modal component
Here's my current code (minus all the irrelevant functionality I've stripped out)
User.jsx:
import { useParams } from 'react-router-dom';
import { Container, Row, Col, Button, Tabs, Tab } from 'react-bootstrap';
import { useAuth } from '../contexts/FollowersContext';
import { FollowingModal } from './partials/user/FollowingModal';
import { ProfileTabAddedPages } from './partials/ProfileTabAddedPages';
import { ProfileTabLikedPages } from './partials/ProfileTabLikedPages';
export function User() {
const { username } = useParams();
const {
checkIsFollowing,
getUserImageById,
getUserProfileByUsername,
} = useAuth();
const handleCloseFollowingDialog = () => setShowFollowingDialog(false);
const handleSetShowFollowingDialog = () => setShowFollowingDialog(true);
const [showFollowingDialog, setShowFollowingDialog] = useState(false);
const [userDetails, setUserDetails] = useState([]);
const [loadingUserDetails, setLoadingUserDetails] = useState(true);
const getUserDetails = async () => {
try {
const data = await getUserProfileByUsername(username);
setUserDetails(data);
setLoadingUserDetails(false);
} catch (error) {
console.log('Error retrieving user profile' + error);
setLoadingUserDetails(false);
}
};
useEffect(() => {
getUserDetails();
// eslint-disable-next-line
}, [loadingUserDetails]);
return (
<div className="container-md clear-header-footer">
<Container flex>
<FollowingModal
showFollowingDialog={showFollowingDialog}
onHideFollowingDialog={handleCloseFollowingDialog}
userid={userDetails?.id}
username={userDetails?.username}
></FollowingModal>
<Row>
<Col>
<h1 className="page-heading">profile</h1>
</Col>
</Row>
<Row>
<Col>
<div className="mx-auto image-placeholder-profile">
</div>
</Col>
<Col>
<h2>{userDetails ? userDetails.displayName : 'display name'}</h2>
<h5>#{userDetails ? userDetails.username : 'username'}</h5>
<p>{userDetails ? userDetails.bio : 'bio'}</p>
<div
onClick={() => handleSetShowFollowingDialog()}
className="clickable-text fit-content"
>
<p>following</p>
</div>
</Col>
</Row>
</Container>
</div>
);
}
FollowingModal.jsx:
import React, { useRef, useState, useEffect, Fragment } from 'react';
import { useAuth as useFollowerContext } from '../../../contexts/FollowersContext';
import { Modal, Card, Col} from 'react-bootstrap';
export function FollowingModal(props) {
const {
getUserFollowing,
} = useFollowerContext();
const [following, setFollowing] = useState([]);
const [loadingFollowers, setLoadingFollowing] = useState(true);
const getFollowing = async () => {
try {
// <----- props?.userid is always undefined at this point ----->
const data = await getUserFollowing(props?.userid);
setFollowing(data);
setLoadingFollowing(false);
} catch (error) {
console.log('getFollowing() error: ' + error);
}
};
useEffect(() => {
getFollowing();
// eslint-disable-next-line
}, [loadingFollowers]);
return (
<Fragment>
<Modal
show={props.showFollowingDialog}
onHide={props.onHideFollowingDialog}
userid={props.userid}
centered
>
<Modal.Header closeButton>
<Modal.Title>Following</Modal.Title>
</Modal.Header>
<Modal.Body>
{following?.map((follower, index) => (
<Col key={index}>
<Card>
<Card.Body>
<span>{follower?.username}</span>
</Card.Body>
</Card>
</Col>
))}
</Modal.Body>
</Modal>
</Fragment>
);
}
getUserFollowing() (in FollowersContext.js):
const getUserFollowing = async (id) => {
try {
const usersFollowingRef = query(
collection(db, 'users', id, 'following')
);
const usersFollowingSnapshot = await getDocs(usersFollowingRef);
if (!usersFollowingSnapshot.empty) {
return usersFollowingSnapshot.docs.map((doc) => doc.data());
}
} catch (error) {
console.error(error);
}
};
Managed to fix it in the end. I changed the user.jsx component so that it check for the userDetails.id value before rendering the modal component:
{userDetails?.id && (
<FollowingModal
showFollowingDialog={showFollowingDialog}
onHideFollowingDialog={handleCloseFollowingDialog}
userid={userDetails?.id}
username={userDetails?.username}
></FollowingModal>
)}
Related
App.js Code
import React, { useState, useEffect } from 'react';
import Products from './components/Products/Products';
import Navbar from './components/Navbar/Navbar';
import { commerce } from './lib/commerce';
const App = () => {
const [products, setProducts] = useState([]);
const [cart, setCart] = useState({});
const fetchProducts = async () => {
const { data } = await commerce.products.list();
setProducts(data);
};
const fetchCart = async () => {
setCart(await commerce.cart.retrieve());
};
const handleAddToCart = async (productId, quantity) => {
const item = await commerce.cart.add(productId, quantity);
setCart(item.cart);
};
useEffect(() => {
fetchProducts();
fetchCart();
}, []);
commerce.cart.empty();
console.log(cart);
return (
<div>
<Navbar totalItems={cart.total_items} />
<Products products={products} onAddToCart={handleAddToCart} />
</div>
);
}
export default App;
NavBar.js
import React from 'react'
import { AppBar, Typography, Toolbar, IconButton, Badge, Menu, MenuItem } from '#material-ui/core'
import { ShoppingCart } from '#material-ui/icons'
import useStyles from './styles'
const Navbar = ({ totalItems }) => {
const classes = useStyles();
return (
<>
<AppBar position='fixed' className={classes.appBar} color='inherit' >
<Toolbar>
{/* THIS WILL BE ON LEFT */}
<Typography variant='h6' color='inherit' className={classes.title}>
<img src="https://image.shutterstock.com/image-photo/image-260nw-611143775.jpg" alt="e-commerce" className={classes.image} height="25px" />
E-store
</Typography>
{/* THIS IS USE TO TAKE AS MUCH SPACE AS WE WANT INORDER TO SEPERTE LEFT AND RIGHT */}
<div className={classes.grow} />
{/* FOR RIGHT PART */}
<div className={classes.button}>
<IconButton aria-label='Show Items' color='inherit'>
<Badge overlap="rectangular" badgeContent={totalItems} color='secondary'>
<ShoppingCart />
</Badge>
</IconButton>
</div>
</Toolbar>
</AppBar>
</>
)``
}
export default Navbar
**commerce.js**
import Commerce from "#chec/commerce.js";
export const commerce = new Commerce(
"HERE_MY_API_KEY"
);
I am getting an error: "Cannot read properties of undefined (reading 'total_items')" but everything looks good on refreshing. On clicking the button the error occurs but after refreshing, the error is gone and I can see my result.
The main problem is that I need to refresh the page. this problem also arises when I add items to the cart. The items get added but are not shown in the console.
Edit: I edited my post to add the whole APP.js component.
import React, { useState, useEffect } from 'react';
import Products from './components/Products/Products';
import Navbar from './components/Navbar/Navbar';
import { commerce } from './lib/commerce';
const App = () => {
const [products, setProducts] = useState([]);
const [cart, setCart] = useState({});
const fetchProducts = async () => {
const { data } = await commerce.products.list();
setProducts(data);
};
const fetchCart = async () => {
const cartItems = await commerce.cart.retrieve();
setCart(cartItems);
};
const handleAddToCart = async (productId, quantity) => {
const item = await commerce.cart.add(productId, quantity);
// I am not sure how your cart object is structured
// but you should add the new item (item.cart)
// to the existing elements.
setCart((cartItems) => [...cartItems, item.cart]);
};
useEffect(() => {
fetchProducts();
fetchCart();
}, []);
useEffect(() => {
console.log(cart);
}, [cart]);
// I don't know why you empty your cart.
// explain in the comments and I'll change it if need be
// commerce.cart.empty();
return (
<div>
{products.length>0 && cart && cart?.total_items && (
<>
<Navbar totalItems={cart.total_items} />
<Products products = {products} onAddToCart=
{handleAddToCart} />
</>
)}
</div>
);
}
export default App;
I want to send State value to another component. They are at same directory. Basically I want to get "selectedvalue" from "Conversation.js" to "Openconversation.js".
Everything's fine but I just need to send that State value (selectedvalue) to Openconversation.js
./components/Conversation.js:
import React, {useState} from 'react'
import { ListGroup } from 'react-bootstrap'
import { useConversations } from '../contexts/ConversationsProvider';
export default function Conversations() {
const { conversations, selectConversationIndex } = useConversations()
**const [selectedvalue, setSelectedValue] = useState('')**
return (
<ListGroup variant="flush">
{conversations.map((conversation, index) => (
<ListGroup.Item
key={index}
action
onClick={() => {selectConversationIndex(index) && setSelectedValue(conversation.recipients.map(r => r.name).join(', '))} }
active={conversation.selected}
>
{conversation.recipients.map(r => r.name).join(', ')}
</ListGroup.Item>
))}
</ListGroup>
)
}
./components/Openconversation.js:
import React, { useState, useCallback, useEffect } from 'react'
import { Form, InputGroup, Button } from 'react-bootstrap'
import { useConversations } from '../contexts/ConversationsProvider';
import Axios from 'axios';
export default function OpenConversation() {
const [text, setText] = useState('');
const [curuser, setCurUser] = useState('');
const [oldchats, setOldChats] = useState([]);
const setRef = useCallback(node => {
if (node) {
node.scrollIntoView({ smooth: true })
}
}, [])
useEffect(() => {
Axios.get('http://localhost:3001/api/get').then((response) =>{
setOldChats(response.data);
})
}, [])
useEffect(()=>{
fetch('http://localhost:8000/api/current_user/', {
headers: {
Authorization: `JWT ${localStorage.getItem('token')}`
}
})
.then(res => res.json())
.then(json => {
setCurUser(json.id);
});
},[])
const { sendMessage, selectedConversation } = useConversations()
function handleSubmit(e) {
e.preventDefault()
sendMessage(
selectedConversation.recipients.map(r => r.id),
text,
selectedConversation.recipients.map(r => {
Axios.post("http://localhost:3001/api/insert", {
content: text,
sender: curuser,
recipient: r.id,
}).then(() => {
alert("saved on MySQL Database!");
});
})
)
setText('')
}
return (
<div className="d-flex flex-column flex-grow-1">
{selectedConversation.messages.map((message, index) => {
const lastMessage = selectedConversation.messages.length - 1 === index
return (
<div>
{oldchats.map(val => {
##### I NEED THAT SELECTEDVALUE DATA FROM CONVERSATION.JS ########
return (val.sender== curuser && val.recipient == selectedvalue) ?
<h2>{val.content}</h2>
:
<div></div>
})}
</div>
)
})}
</div>
</div>
<Form onSubmit={handleSubmit}>
<Form.Group className="m-2">
<InputGroup>
<Form.Control
as="textarea"
required
value={text}
onChange={e => setText(e.target.value)}
/>
<InputGroup.Append>
<Button type="submit">Send</Button>
</InputGroup.Append>
</InputGroup>
</Form.Group>
</Form>
</div>
)
}
It looks like in your Openconversation.js file you're not importing useContext from react and then assigning it to something like:
const conversationProviderContext = useContext(ConversationsProvider);
Also make sure that you're using the <ConversationsProvider.Provider/> component higher up above the <Openconversation/> component.
You should probably refer to the docs: https://reactjs.org/docs/context.html
I have filtered the products and on submitting the search term, am showing the results in a new page using history.push() property.
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { IoIosSearch } from 'react-icons/io';
import { useHistory } from "react-router-dom";
import './style.css';
/**
* #author
* #function Search
*/
const Search = (props) => {
const product = useSelector(state => state.product);
let { products , filteredProducts } = product;
const [searchTerm, setSearchTerm] = useState('');
const onChangeSearch = (e) => {
setSearchTerm(e.currentTarget.value);
}
const isEmpty = searchTerm.match(/^\s*$/);
if(!isEmpty) {
filteredProducts = products.filter( function(prod) {
return prod.name.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase().trim())
})
}
const history = useHistory();
const display = !isEmpty
const handleSubmit =(e) => {
e.preventDefault();
if( !isEmpty ) {
history.push(`/search/search_term=${searchTerm}/`, { filteredProducts })
}
setSearchTerm('');
}
return (
<div>
<form onSubmit={handleSubmit}>
<div className="searchInputContainer">
<input
className="searchInput"
placeholder={'What are you looking for...'}
value={searchTerm}
onChange={onChangeSearch}
/>
<div className="searchIconContainer">
<IoIosSearch
style={{
color: 'black',
fontSize: '22px'
}}
onClick={handleSubmit}
/>
</div>
</div>
</form>
{
display && <div className="searchResultsCont">
{filteredProducts.map((prod, index) => (<div key={index}>{prod.name}</div>))}
</div>
}
</div>
);
}
export default Search
On the new page this is the code :
import React from 'react';
import Layout from '../../components/Layout';
const SearchScreen = ({location}) => {
const products = location.state.filteredProducts;
const show = products.length > 0
return (
<Layout>
<div>
{
show ? products.map((prod, index) => (<div key={index}>{prod.name}</div>)) : <div>No items found</div>
}
</div>
</Layout>
)
}
export default SearchScreen
The problem comes when I copy and paste the URL to another new page, or like when I email others the URL the error becomes " Cannot read property 'filteredProducts' of undefined ". Using this method I understand that the results (filtered products) have not been pushed through the function history.push() that's why it is undefined, how can I make this possible?
I changed the whole aspect to filtering the products from the back-end..
It worked
I have a component into which I am fetching data. As expected the component renders first and then the useEffect. The trouble is that this useEffect fetches data that I need rendered in my component. Anyway I can make the content in the component load after the useEffect propagates data to the useState?
import React, { useEffect, useState } from 'react';
import Layout from '../components/global/layout/Layout';
import axios from 'axios';
import { Col, Row } from 'react-bootstrap';
import styles from '../components/page-css/menu.module.css';
import MenuItemHolder from '../components/menu-page/menuItemHolder/menuItemHolder';
const Menu = () => {
const [ menuItems, setMenuItems ] = useState(false);
useEffect(() => {
const fetchData = async () => {
const result = await axios('https://tahina-test.herokuapp.com/doggos');
setMenuItems(result.data);
};
fetchData();
}, []);
return (
<Layout textColor="white">
<Col>
<Row className={styles.menuHolder}>
<Col className={styles.menu} xs="11" md="8">
<h1>Menu</h1>
<Row>
{menuItems.records.map((item) => (
<MenuItemHolder name={item.Name} price={item.UnitPrice} />
))}
</Row>
</Col>
</Row>
</Col>
</Layout>
);
};
export default Menu;
As I understand your question, you are asking how to only render the MenuItemHolder components when the data has been fetched and the state has been populated with this data.
There are two ways to achieve this:
#1 Initialize menuItems.records with an empty array:
const [ menuItems, setMenuItems ] = useState({records: []});
such that on the initial render, menuItems.records.map([...]) will return an empty.
#2 Render the MenuItemHolder components based on a condition:
<Row>
{menuItems && menuItems.records.map((item) => (
<MenuItemHolder name={item.Name} price={item.UnitPrice} />
))}
</Row>
This will work since the initial value of menuItems is false.
You can initialize the menuItems beforehand, so accessing menuItems.records will be valid.
const Menu = () => {
const [menuItems, setMenuItems] = useState({ records: [] });
useEffect(() => {
//...
fetchData();
}, []);
// records are empty until data fetch
return (
<>
{menuItems.records.map((item) => {
/*...*/
})}
</>
);
};
export default Menu;
Or just use conditional rendering:
const Menu = () => {
const [menuItems, setMenuItems] = useState();
useEffect(() => {
//...
fetchData();
}, []);
// Conditional rendering
return (
<>
// Same as menuItems && menuItems.records
{menuItems?.records.map((item) => {
/*...*/
})}
</>
);
};
export default Menu;
Add loading state to render loading element while fetchData is fetching data from server.
import React, { useEffect, useState } from 'react';
import Layout from '../components/global/layout/Layout';
import axios from 'axios';
import { Col, Row } from 'react-bootstrap';
import styles from '../components/page-css/menu.module.css';
import MenuItemHolder from '../components/menu-page/menuItemHolder/menuItemHolder';
const Menu = () => {
const [ loading, setLoading ] = useState(false);
const [ menuItems, setMenuItems ] = useState({records: []});
const fetchData = async () => {
setLoading(true);
const result = await axios('https://tahina-test.herokuapp.com/doggos');
setLoading(false);
setMenuItems(result.data);
};
useEffect(() => {
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Layout textColor="white">
<Col>
<Row className={styles.menuHolder}>
{loading ? (
<Col className={styles.menu} xs="11" md="8">
Loading, please wait...
</Col>
) : (
<Col className={styles.menu} xs="11" md="8">
<h1>Menu</h1>
{menuItems.records.map((item, index) => (
<Row key={index}>
<MenuItemHolder name={item.Name} price={item.UnitPrice} />
</Row>
))}
</Col>
)}
</Row>
</Col>
</Layout>
);
};
export default Menu;
I have some trouble with my code. I made an app where I use an API last fm and I want to add a rating button, I get few things from Google. Rating is displayed where I want to be, I can console log him, but it's on external file and I have no idea how to modify rate state from my app.js. Here is my code:
App.js
import React, { Component } from 'react';
import AppBar from '#material-ui/core/AppBar';
import Toolbar from '#material-ui/core/Toolbar';
import Typography from '#material-ui/core/Typography';
import { getArtists } from './services/api';
import {
TextField,
Button,
List
} from '#material-ui/core';
import { ArtistCard } from './components/ArtistCard';
import { SearchResult } from './components/SearchResult';
import './App.css';
import { get } from 'https';
const handleChangeRate = (state) => {
this.setState({rate: state})
}
const isEmpty = (str) => str.length === 0;
class App extends Component {
state = {
searchTerm: '',
savedArtists: [],
rate:[]
}
componentDidMount() {
const existing = localStorage.getItem('savedArtists')
if (existing) {
this.setState({ savedArtists: JSON.parse(existing) })
}
}
onTextChange = (event) => {
const value = event.target.value;
this.setState({ searchTerm: value });
}
search = async (terms) => {
const artists = await getArtists(terms);
this.setState({ artists: artists })
}
onSearchClick = () => {
this.search(this.state.searchTerm);
}
clearSearch = () => {
this.setState({
searchTerm: '',
artists: []
})
}
updateArtists = (newArtists) => {
this.setState({ savedArtists: newArtists })
localStorage.setItem('savedArtists', JSON.stringify(newArtists));
}
deleteArtist = (artist) => {
const result = this.state.savedArtists.filter(item => item.name !== artist.name);
this.updateArtists(result);
}
onResultClick = (artist) => {
this.clearSearch();
const savedArtists = this.state.savedArtists;
savedArtists.push(artist);
this.updateArtists(savedArtists);
}
handleChangeRate = (state) => {
this.setState({rate: state})
}
render() {
const results = this.state.artists || [];
return (
<div className="App">
<header className="App-header">
<AppBar position="static" color="primary">
<Toolbar className="search-bar">
<Typography variant="h6" color="inherit">
Photos
</Typography>
<TextField
placeholder="Search on Last.fm"
className="search-input"
onChange={this.onTextChange}
value={this.state.searchTerm}
/>
<Button
onClick={this.onSearchClick}
variant="contained"
color="secondary"
disabled={isEmpty(this.state.searchTerm)}
>
Search
</Button>
{!isEmpty(this.state.searchTerm) && (
<Button
onClick={this.clearSearch}
variant="contained"
>
Clear
</Button>)
}
</Toolbar>
</AppBar>
</header>
<List className="search-results">
{
results.map((artist, index) => {
return <SearchResult key={index} artist={artist} onResultClick={this.onResultClick} />
})
}
</List>
<div className="artist-container">
{
this.state.savedArtists.map((artist, index) => {
return <ArtistCard artist={artist} key={index} deleteArtist={this.deleteArtist} onChangeRating={this.handleChangeRate} />
})
}
</div>
</div>
);
}
}
export default App;
artistCard.js
import React from 'react';
import { Card, CardContent, CardActions, Button } from '#material-ui/core'
import ReactStars from 'react-stars'
export const ratingChanged = (newRating) => {
const { onChangeRating } = this.props;
onChangeRating(newRating);
}
export const ArtistCard = (props) => {
const { artist, deleteArtist } = props;
console.log(artist.cardImage)
return (
<Card className="artist-card">
<div className="image-container">
<img src={artist.cardImage} alt={artist.name} />
</div>
<CardContent>
<h3>{artist.name}</h3>
<p>{artist.listeners} listeners.</p>
<ReactStars
count = {5}
onChange={ratingChanged}
size={27}
color2 ={'#ffd700'}
/>
</CardContent>
<CardActions>
<Button size="small" color="primary">
Share
</Button>
<Button size="small" color="secondary" onClick={() => deleteArtist(artist)}>
Delete
</Button>
</CardActions>
</Card>
)
}
You need to pass the function to change State to artistCard as props
In App.js add the following fucntion
const handleChangeRate = (state) => {
this.setState(rate: state)
}
and Pass the same as props to ArtistCard like specified
<ArtistCard artist={artist} key={index} deleteArtist={this.deleteArtist} onChangeRating={this.handleChangeRate} />
And in artistCard.js change ratingChanged method to
const ratingChanged = (newRating) => {
const { onChangeRating } = this.props;
onChangeRatng(newRating);
}
PS: This answer is based on the understanding i gained after going through this question, If this is not the requirement Please feel free to comment
EDIT
const handleChangeRate = (state) => {
this.setState(rate: state)
}
try adding the value prop to ReactStart like specified below
<ReactStars
value={this.props.rate}
count={5}
onChange={ratingChanged}
size={24}
color2={'#ffd700'}
/>
Pass rate state to artist card component as prop like specified below
<ArtistCard artist={artist} key={index} deleteArtist= {this.deleteArtist} onChangeRating={this.handleChangeRate} rate={this.state.rate} />
EDIT
cardComponent.js
import React from 'react';
import ReactStars from 'react-stars'
export const ArtistCard = (props) => {
const { artist, deleteArtist, onChangeRating } = props;
console.log(props.rating)
return (
<ReactStars
value={props.rating}
count = {5}
onChange={(newRating) => onChangeRating(newRating)}
size={27}
color2 ={'#ffd700'}
/>)}
App.js
handleChangeRate = (state) => {
this.setState({rate: state})
}
<ArtistCard artist={'artist'} key={'index'} rating={this.state.rate} deleteArtist={this.deleteArtist} onChangeRating={this.handleChangeRate} />
FINAL EDIT
Changes made in your code
App.js
modified state object to
state = {
searchTerm: '',
savedArtists: [],
rate: ''
}
Artistcard component line to
<ArtistCard rate={this.state.rate} artist={artist} key={index} onChangeRating={(val) => {this.setState({ rate: val })}} deleteArtist={this.deleteArtist} />
In ArtistCard.js
rendering reactstart component like this
<ReactStars
value={props.rate}
count = {5}
onChange={(newRating) => onChangeRating(newRating)}
size={27}
color2 ={'#ffd700'}
/>