Can Anyone hazard a guess as to why the following optimisticResponse/update code (question continues after code):
AddToCart.js
const ADD_TO_CART_MUTATION = gql`
mutation addToCart($id: ID!) {
addToCart(id: $id) {
id
quantity
}
}
`;
class AddToCart extends React.Component {
outOfStock(){
alert('This item is out of stock.');
};
update = (cache, { data: { addToCart } }) => {
const data = cache.readQuery({ query: CURRENT_USER_QUERY });
data.me.cart.push(addToCart);
cache.writeQuery({ query: CURRENT_USER_QUERY, data });
};
render() {
const { id, quantity, title, image, price } = this.props;
return (
<Mutation
mutation={ADD_TO_CART_MUTATION}
variables={{
id,
}}
optimisticResponse={{
__typename: 'Mutation',
addToCart: {
__typename: 'CartItem',
id: '-1',
quantity: 1,
item: {
__typename: 'Item',
id,
price,
image,
title,
description: 'test',
mainDescription: 'test',
quantity,
}
}
}}
update={this.update}
refetchQueries={[{ query: CURRENT_USER_QUERY }, { query: ALL_ITEMS_QUERY }]}
>
{(addToCart, { loading }) => {
if (quantity >= 1) {
return (<button disabled={loading} onClick={() => {
addToCart().catch(err => alert(err.message));
}}>
Add{loading && 'ing'} To Cart 🛒
</button>)
} else {
return (<button>
Out of Stock 🛒
</button>)
}
}}
</Mutation>
);
}
}
export default AddToCart;
AddToCart parent component (Item.js)
export default class Item extends Component {
static propTypes = {
item: PropTypes.object.isRequired,
};
render() {
const { item } = this.props;
return (
<User>
{({ data: { me } }) => {
let hasPerms;
hasPerms = (me && me === null) ? false : (me && me.permissions.some(permission => ['ADMIN'].includes(permission)));
return (
<ItemStyles>
{item.image && <img src={item.image} alt={item.title} />}
<Title>
<Link
href={{
pathname: '/item',
query: { id: item.id },
}}
>
<a>{item.title}</a>
</Link>
</Title>
<PriceTag>{formatMoney(item.price)}</PriceTag>
<p>{item.description} { (item.quantity <= 10 && item.quantity !== 0) && `- (${item.quantity} in stock)` }</p>
<div className="buttonList">
<AddToCart id={item.id} quantity={item.quantity} image={item.image} title={item.title} price={item.price} />
{hasPerms && (
<>
<Link
href={{
pathname: 'update',
query: { id: item.id },
}}
>
<a>Edit ✏️</a>
</Link>
<DeleteItem id={item.id}>Delete This Item</DeleteItem>
</>
)}
</div>
<div className="buttonList">
</div>
</ItemStyles>
);
}}
</User>
);
}
}
Item parent (Items.js)
class Items extends Component {
render() {
return (
<Center>
<Pagination page={this.props.page} />
<Query
query={ALL_ITEMS_QUERY}
variables={{
skip: this.props.page * perPage - perPage,
}}
>
{({ data, error, loading }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ItemsList>{data.items.map((item, i) => <Item item={item} key={item.id}></Item>)}</ItemsList>
);
}}
</Query>
<Pagination page={this.props.page} />
</Center>
);
}
}
export default Items;
causes my UI to rerender (component unmounts) after adding an item to an empty cart (See instructions to replicate)?
Go to https://flamingo-next-production.herokuapp.com.
login using, Username: testing123#123.com, Password:testing123.
click 'My cart' link and delete any items in there. Leave cart window open.
click Shop link and add an item. Observe the open cart window/UI after you have clicked add (You may have to wait a few moments before observing change).
Why does this happen and what's the best way to rectify it??
Related
I have search filter and categories. I just want to have a possibility to reset state in single page application.
Due to React.js I guess I do everything correct to pass state from parent to child and then from child to parent. But, unfortunately, something is going wrong. I tried a lot and what I discovered, that onAddCategory() in DropdownGroup doesn't update current state.
Sorry in advance, I add whole code, my be something there could affect this. But i guess you can see first halfs of two codes and it will be enough.
Thank you in advance.
I have parent component:
class DropdownGroup extends React.Component {
constructor(props) {
super(props);
this.state = {
categories: [], // we have empty array, that pass to CategoryDropdown
};
this.onAddCategory = this.onAddCategory.bind(this);
}
onAddCategory(newCategory) {
this.setState(() => ({
categories: newCategory,
}));
}
onSelectCategory(path) {
this.props.onChangeEvents(path);
}
render() {
const months = ['January', 'February' ... ];
const eventsType = ['Party', 'Karaoke ... ];
const { categories } = this.state;
return (
<ButtonToolbar className="justify-content-center pb-4 pt-4">
{ console.log(categories) }
<CategoryDropdown
items={eventsType}
homePath="events"
path="events/categories/"
categories={categories} // here we pass our empty array (or updated later)
addCategories={this.onAddCategory} // this is what helps to update our array
onApply={(path) => this.onSelectCategory(path)}
/>
<MyDropdown
id="sort-by-month"
name="By month"
items={months}
onSelect={(e) => this.onSelectCategory(`events/month/${e}`)}
/>
<DropdownWithDate
oneDate="events/date/"
rangeDate="events/dates?from="
onApply={(path) => this.onSelectCategory(path)}
/>
<Button
onClick={() => this.setState({ categories: [] })} // here we can reset the value of our array
className="m-button ml-5"
>
Reset
</Button>
</ButtonToolbar>
);
}
}
DropdownGroup.propTypes = {
onChangeEvents: PropTypes.any.isRequired,
};
export default DropdownGroup;
and this is child component
class CategoryDropdown extends Component {
constructor(props) {
super(props);
this.state = {
visible: false,
selected: this.props.categories, // here we get values from props (now empty, then updated values)
};
this.onVisibleChange = this.onVisibleChange.bind(this);
}
onVisibleChange(visible) {
this.setState({
visible: visible,
});
}
saveSelected(selectedKeys) {
this.setState({
selected: selectedKeys,
});
}
addCategories() {
this.props.addCategories(this.state.selected); // here props are updated
}
confirm() {
const { selected } = this.state;
this.addCategories(this.state.selected);
const { homePath, path } = this.props;
if (selected.length > 0) {
this.props.onApply(path + selected);
} else {
this.props.onApply(homePath);
}
this.onVisibleChange(false);
}
render() {
const { visible } = this.state;
const { items } = this.props;
const menu = (
<Menu
multiple
onSelect={(e) => { this.saveSelected(e.selectedKeys); }}
onDeselect={(e) => { this.saveSelected(e.selectedKeys); }}
>
{items.map((item) => (
<MenuItem
key={item.replace('\u0020', '\u005f').toLowerCase()}
>
{item}
</MenuItem>
))}
<Divider />
<MenuItem disabled>
<Container
className="text-center "
style={{
cursor: 'pointer',
pointerEvents: 'visible',
}}
onClick={() => {
this.confirm();
}}
>
Select
</Container>
</MenuItem>
</Menu>
);
return (
<Dropdown
trigger={['click']}
onVisibleChange={this.onVisibleChange}
visible={visible}
closeOnSelect={false}
overlay={menu}
>
<Button className="m-button">By Category</Button>
</Dropdown>
);
}
}
CategoryDropdown.propTypes = {
onApply: PropTypes.any.isRequired,
items: PropTypes.any.isRequired,
path: PropTypes.string.isRequired,
homePath: PropTypes.string.isRequired,
categories: PropTypes.array.isRequired,
addCategories: PropTypes.any.isRequired,
};
export default CategoryDropdown;
I have five Users in the array.
The code below displays each users info from the arrays when pop up button is clicked and everything works fine.
Now I have created a form to update each user's age based on their respective person Id on form submission via call to nodejs
backend. Am actually getting the result from nodejs backend..
Here is my issue.
Each time I entered age in the input and click on submission button Eg. for user 1. Instead of the age result to
appear near that very user 's name in the space provided in the button, it will appears on the body of the page as can be seen from
screenshots provided.
If call it as props For instance {this.props.messages.personAge}
as per below
<button
onClick={() => this.open(this.props.data.id, this.props.data.name)}
>
(Age should Appear Here-- ({this.props.messages.personAge})--)
{this.props.data.name}
</button>
It shows error
TypeError: Cannot read property 'personAge' of undefined
at User.render
Here is how am getting the response from nodejs server
componentDidMount(){
this.socket = io('http://localhost:8080');
this.socket.on('response message', function(data){
addAge(data);
});
const addAge = data => {
console.log(data);
//this.setState({messages: [...this.state.messages, data]});
this.setState({messages: [data]});
};
}
below is how am displaying the age result for each unique user
{this.state.messages.map((message, i) => {
//if (message.personId == this.props.data.id) {
//if (message.personId == person.id) {
if (message.personId) {
return (
<div key={i}>
<div>
({message.personAge}--years)
</div>
</div>
)
}
})}
</ul>
Here is the Entire Code
import React, { Component, Fragment } from "react";
import { render } from "react-dom";
import { Link } from 'react-router-dom';
import axios from 'axios';
import io from "socket.io-client";
class User extends React.Component {
open = () => this.props.open(this.props.data.id, this.props.data.name);
render() {
return (
<React.Fragment>
<div key={this.props.data.id}>
<button
onClick={() => this.open(this.props.data.id, this.props.data.name)}
>
(Age should Appear Here-- ({this.props.messages})--)
{this.props.data.name}
</button>
</div>
</React.Fragment>
);
}
}
class OpenedUser extends React.Component {
constructor(props) {
super(props);
this.state = {
hidden: false,
personId: '',
personAge: '',
};
}
componentDidMount(){
this.socket = io('http://localhost:8080');
var userId= this.props.data.id;
}
sendPost = (personId,personAge) => {
alert(personId);
alert(personAge);
this.socket.emit('messageUpdate', {
personId: personId,
personAge: personAge,
});
this.setState({personId: ''});
this.setState({personAge: ''});
}
toggleHidden = () =>
this.setState(prevState => ({ hidden: !prevState.hidden }));
close = () => this.props.close(this.props.data.id);
render() {
return (
<div key={this.props.data.id} style={{ display: "inline-block" }}>
<div className="wrap_head">
<button onClick={this.close}>close</button>
<div>user {this.props.data.id}</div>
<div>name {this.props.data.name}</div>
{this.state.hidden ? null : (
<div className="wrap">
<div className="wrap_body">Update Age Info</div>
<div> </div>
<div>
<label></label>
<input type="text" placeholder="personAge" value={this.state.personAge} onChange={ev => this.setState({personAge: ev.target.value})}/>
<br/>
<span onClick={ () => this.sendPost(this.props.data.id, this.state.personAge)} className="btn btn-primary">Update Age</span>
</div>
</div>
)}
</div>
</div>
);
}
}
class App extends React.Component {
constructor() {
super();
this.state = {
showingAlert_UserTyping: false,
shown: true,
activeIds: [],
messages: [],
data: [
{ id: 1, name: "user 1" },
{ id: 2, name: "user 2" },
{ id: 3, name: "user 3" },
{ id: 4, name: "user 4" },
{ id: 5, name: "user 5" }
]
};
}
componentDidMount(){
this.socket = io('http://localhost:8080');
this.socket.on('response message', function(data){
addAge(data);
console.log(' am add message' +data);
});
const addAge = data => {
console.log(data);
//this.setState({messages: [...this.state.messages, data]});
this.setState({messages: [data]});
};
} // close component didmount
toggle() {
this.setState({
shown: !this.state.shown
});
}
open = (id,name) => {
this.setState(prevState => ({
activeIds: prevState.activeIds.find(user => user === id)
? prevState.activeIds
: [...prevState.activeIds, id]
}));
};
close = id => {
this.setState(prevState => ({
activeIds: prevState.activeIds.filter(user => user !== id)
}));
};
renderUser = id => {
const user = this.state.data.find(user => user.id === id);
if (!user) {
return null;
}
return (
<OpenedUser messages={this.state.messages}
key={user.id}
data={user}
close={this.close}
/>
);
};
renderActiveUser = () => {
return (
<div style={{ position: "fixed", bottom: 0, right: 0 }}>
{this.state.activeIds.map(id => this.renderUser(id))}
</div>
);
};
render() {
return (
<div>
<ul>
{this.state.messages.map((message, i) => {
//if (message.personId == this.props.data.id) {
//if (message.personId == person.id) {
if (message.personId) {
return (
<div key={i}>
<div>
({message.personAge}--years)
</div>
</div>
)
}
})}
</ul>
{this.state.data.map(person => {
return (
<User key={person.id} data={person} open={this.open} />
);
})}
{this.state.activeIds.length !== 0 && this.renderActiveUser()}
</div>
);
}
}
Here is how I solved the issue:
I created a const resultdata and using map() and Filter() function.
Here is how I initialized the the variable resultdata and then pass it within state.data.map() method
const resultdata = this.state.messages.filter(res => res.personId == person.id).map(res => res.personAge));
I have a nested route component which is getting displayed at the bottom of the parent component. I want to display this component in a new page. Here is my code-
CategoryList.js
class CategoryList extends Component {
state = {
categories: []
}
componentDidMount() {
fetch('http://localhost:8080/testcategory')
.then(results => {
return results.json();
}).then(data => {
this.setState({ categories: data.categories });
})
.catch(error => {
this.setState({ error: true });
});
}
categorySelectedHandler = (id) => {
this.props.history.replace('/testcategory/' + id);
}
render() {
let categories = <p style={{ textAlign: 'center' }}>Something went wrong!</p>;
if (!this.state.error) {
categories = this.state.categories.map(category => {
{this.props.children}
return (
<Table.Row key={category.id}>
<Table.Cell>{category.name}</Table.Cell>
<Table.Cell>{category.id}</Table.Cell>
<Table.Cell> <Button icon labelPosition='left' onClick={() => this.categorySelectedHandler(category.id)}>show</Button></Table.Cell>
{/* Tried this as well
<Table.Cell>
<Link to={"/category/"+category.id}>
<Button icon labelPosition='left' >Show</Button>
</Link>
</Table.Cell> */}
</Table.Row>
)
})
}
return (
<div>
<Table stackable>
<Table.Header>
<Table.Row >
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>ID</Table.HeaderCell>
<Table.HeaderCell>Operations</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{categories}
{this.props.children}
</Table.Body>
</Table>
<Route path={this.props.match.url + '/:id'} exact component={CategoryDetails} />
</div>
);
}
}
export default CategoryList;
CategoryDetails.js
import React, { Component } from 'react';
import './CategoryDetails.css';
class CategoryDetails extends Component {
state = { loadedCategory: null }
componentDidMount() {
this.loadData();
}
componentDidUpdate() {
this.loadData();
}
shouldComponentUpdate(nextProps, nextState) {
return nextProps.match.params.id != nextState.loadedCategory.id ;
}
loadData=() =>{
if (this.props.match.params.id) {
if (!this.state.loadedCategory || (this.state.loadedCategory && this.state.loadedCategory.id !== +this.props.match.params.id)) {
fetch('http://localhost:8080/testcategory/' + this.props.match.params.id)
.then(results => {
return results.json();
}).then(data => {
this.setState({ loadedCategory: data});
})
.catch(error => {
this.setState({ error: true });
});
}
}
}
render() {
let category = <p style={{ textAlign: 'center' }}>Please select a Post!</p>;
if (this.props.match.params.id) {
category = <p style={{ textAlign: 'center' }}>Loading...!</p>;
}
if (this.state.loadedCategory) {
category = (
<div className="CategoryDetails">
<h1>{this.state.loadedCategory.name}</h1>
<p>{this.state.loadedCategory.code}</p>
<p>{this.state.loadedCategory.id}</p>
{this.props.children}
</div>
);
}
return (category);
}
}
export default CategoryDetails;
remove CategoryDetails Route from CategoryList file and move it to the file where Route specified for CategoryList
<Route path={this.props.match.url + '/:id'} exact component={CategoryDetails} />
<Route path={this.props.match.url + '/:dummyid'} exact component={CategoryList} />
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}>
I am trying to change the route based on the dropdown option in the breadcrumb.Any suggestions would be appreciated.
This is the component that is getting the first option but I am not sure how to gran the other options after the dropdown is generated.
const MenuItemViews = ({ params: { category, subCategory, item }, children }) => {
const menuItem = sideNavData.lookupItem(category, subCategory, item);
console.log(menuItem);
console.info(children);
return (
<div>
{
menuItem.name === 'Bill'
? <div>
<h2>Labels</h2>
{
!children
? <Link to={`/${category}/${subCategory}/${item}/${menuItem.childItems[0].name}`} >
<Image src={menuItem.content} />
</Link>
: children
}
</div>
: <ContentContainer>
<h1>{menuItem.name}</h1>
<Image src={menuItem.content} alt='item' />
</ContentContainer>
}
</div>
);
};
this is the component that is displaying the breadcrumbs.
const labelDropdownOptions = [
{ key: 'OptionOne', value: 'OptionOne', text: 'OptionOne' },
{ key: 'OptionTwo', value: 'OptionTwo', text: 'OptionTwo' },
{ key: 'OptionThree', value: 'OptionThree', text: 'OptionThree' },
];
class TopBar extends Component {
resolver = (key) => {
if (key === 'Home') {
return key;
}
return this.props.params[key];
}
dropdownLink = (link, key, text, index, routes) => {
console.log(routes);
if (text === 'OptionOne') {
return (
<Dropdown defaultValue={'OptionOne'} key={key} options={labelDropdownOptions} />
);
}
return <Link key={key} to={link}>{text}</Link>;
}
render() {
const { routes, params } = this.props;
return (
<TopBarHeader>
<IndexLink to='/'>
<HomeIcon><Icon name='home' /></HomeIcon>
</IndexLink>
<BreadcrumbWrapper>
<Breadcrumbs
createLink={this.dropdownLink}
params={params}
resolver={this.resolver}
routes={routes}
/>
</BreadcrumbWrapper>
</TopBarHeader>
);
}
}
I was able to do this by passing this.props.router.push into the onClick prop and specifying the value.
class TopBar extends Component {
resolver = (key) => {
if (key === 'Home') {
return key;
}
return this.props.params[key];
}
dropdownLink = (link, key, text, index, routes) => {
const category = sideNavData.lookupCategory(this.props.category);
if (link === '/TabTwo/Names/Bill/OptionOne' || link === '/TabTwo/Names/Bill/OptionTwo' || link === '/TabTwo/Names/Bill/OptionThree') {
return (
<span key={index}>
{
Object.keys(category).map((subCategory, i) => {
return (
<span key={i}>
{
Object.keys(category[subCategory]).map((item, itemIndex) => (
<span key={itemIndex}>
{
category[subCategory][item].name === 'Bill'
? <Dropdown
defaultValue={'OptionOne'}
options={category[subCategory][item].childItems}
onChange={(event, data) => { this.props.router.push(`/${this.props.category}/${subCategory}/${category[subCategory][item].name}/${data.value}`); }}
/>
: null
}
</span>
))
}
</span>
);
})
}
</span>
);
}
return <Link key={key} to={link}>{text}</Link>;
}
render() {
const { routes, params } = this.props;
return (
<TopBarHeader>
<IndexLink to='/'>
<HomeIcon><Icon name='home' /></HomeIcon>
</IndexLink>
<BreadcrumbWrapper>
<Breadcrumbs
createLink={this.dropdownLink}
params={params}
resolver={this.resolver}
routes={routes}
/>
</BreadcrumbWrapper>
</TopBarHeader>
);
}
}