I am creating an app using React and Apollo Graphql. Part of my app consist of showing a list of options to the user so he can pick one. Once he picks one of them, the other options are hidden.
Here is my code:
/**
* Renders a list of simple products.
*/
export default function SimplesList(props: Props) {
return (
<Box>
{props.childProducts
.filter(child => showProduct(props.parentProduct, child))
.map(child => (
<SingleSimple
key={child.id}
product={child}
menuItemCacheId={props.menuItemCacheId}
parentCacheId={props.parentProduct.id}
/>
))}
</Box>
);
}
And the actual element:
export default function SingleSimple(props: Props) {
const classes = useStyles();
const [ref, setRef] = useState(null);
const [flipQuantity] = useFlipChosenProductQuantityMutation({
variables: {
input: {
productCacheId: props.product.id,
parentCacheId: props.parentCacheId,
menuItemCacheId: props.menuItemCacheId,
},
},
onError: err => {
if (process.env.NODE_ENV !== 'test') {
console.error('Error executing Flip Chosen Product Quantity Mutation', err);
Sentry.setExtras({ error: err, query: 'useFlipChosenProductQuantityMutation' });
Sentry.captureException(err);
}
},
});
const [validateProduct] = useValidateProductMutation({
variables: { productCacheId: props.menuItemCacheId },
onError: err => {
if (process.env.NODE_ENV !== 'test') {
console.error('Error executing Validate Product Mutation', err);
Sentry.setExtras({ error: err, query: 'useValidateProductMutation' });
Sentry.captureException(err);
}
},
});
const refCallback = useCallback(node => {
setRef(node);
}, []);
const scrollToElement = useCallback(() => {
if (ref) {
ref.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}, [ref]);
const onClickHandler = useCallback(async () => {
await flipQuantity();
if (props.product.isValid !== ProductValidationStatus.Unknown) {
validateProduct();
}
scrollToElement();
}, [flipQuantity, props.product.isValid, validateProduct, scrollToElement]);
return (
<ListItem className={classes.root}>
<div ref={refCallback}>
<Box display='flex' alignItems='center' onClick={onClickHandler}>
<Radio
edge='start'
checked={props.product.chosenQuantity > 0}
tabIndex={-1}
inputProps={{ 'aria-labelledby': props.product.name! }}
color='primary'
size='medium'
/>
<ListItemText
className={classes.text}
primary={props.product.name}
primaryTypographyProps={{ variant: 'body2' }}
/>
<ListItemText
className={classes.price}
primary={getProductPrice(props.product)}
primaryTypographyProps={{ variant: 'body2', noWrap: true, align: 'right' }}
/>
</Box>
{props.product.chosenQuantity > 0 &&
props.product.subproducts &&
props.product.subproducts.map(subproduct => (
<ListItem component='div' className={classes.multiLevelChoosable} key={subproduct!.id}>
<Choosable
product={subproduct!}
parentCacheId={props.product.id}
menuItemCacheId={props.menuItemCacheId}
is2ndLevel={true}
/>
</ListItem>
))}
</div>
</ListItem>
);
}
My problem is this: once the user selects an element from the list, I would like to scroll the window to that element, because he will have several lists to choose from and he can get lost when choosing them. However my components are using this flow:
1- The user clicks on a given simple element.
2- This click fires an async mutation that chooses this element over the others.
3- The application state is updated and all components from the list are re-created (the ones that were not selected are filtered out and the one that was selected is displayed).
4- On the re-creation is done, I would like to scroll to the selected component.
The thing is that when the flipQuantity quantity mutation finishes its execution, I call the scrollToElement callback, but the ref it contains is for the unselected element, that is no longer rendered on the screen, since the new one will be recreated by the SimplesList component.
How can I fire the scrollIntoView function on the most up-to-date component?
UPDATE:
Same code, but with the useRef hook:
export default function SingleSimple(props: Props) {
const classes = useStyles();
const ref = useRef(null);
const [flipQuantity] = useFlipChosenProductQuantityMutation({
variables: {
input: {
productCacheId: props.product.id,
parentCacheId: props.parentCacheId,
menuItemCacheId: props.menuItemCacheId,
},
},
onError: err => {
if (process.env.NODE_ENV !== 'test') {
console.error('Error executing Flip Chosen Product Quantity Mutation', err);
Sentry.setExtras({ error: err, query: 'useFlipChosenProductQuantityMutation' });
Sentry.captureException(err);
}
},
});
const [validateProduct] = useValidateProductMutation({
variables: { productCacheId: props.menuItemCacheId },
onError: err => {
if (process.env.NODE_ENV !== 'test') {
console.error('Error executing Validate Product Mutation', err);
Sentry.setExtras({ error: err, query: 'useValidateProductMutation' });
Sentry.captureException(err);
}
},
});
const scrollToElement = useCallback(() => {
if (ref && ref.current) {
ref.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}, [ref]);
const onClickHandler = useCallback(async () => {
await flipQuantity();
if (props.product.isValid !== ProductValidationStatus.Unknown) {
validateProduct();
}
scrollToElement();
}, [flipQuantity, props.product.isValid, validateProduct, scrollToElement]);
return (
<ListItem className={classes.root}>
<div ref={ref}>
<Box display='flex' alignItems='center' onClick={onClickHandler}>
<Radio
edge='start'
checked={props.product.chosenQuantity > 0}
tabIndex={-1}
inputProps={{ 'aria-labelledby': props.product.name! }}
color='primary'
size='medium'
/>
<ListItemText
className={classes.text}
primary={props.product.name}
primaryTypographyProps={{ variant: 'body2' }}
/>
<ListItemText
className={classes.price}
primary={getProductPrice(props.product)}
primaryTypographyProps={{ variant: 'body2', noWrap: true, align: 'right' }}
/>
</Box>
{props.product.chosenQuantity > 0 &&
props.product.subproducts &&
props.product.subproducts.map(subproduct => (
<ListItem component='div' className={classes.multiLevelChoosable} key={subproduct!.id}>
<Choosable
product={subproduct!}
parentCacheId={props.product.id}
menuItemCacheId={props.menuItemCacheId}
is2ndLevel={true}
/>
</ListItem>
))}
</div>
</ListItem>
);
}
UPDATE 2:
I changed my component once again as per Kornflexx suggestion, but it is still not working:
export default function SingleSimple(props: Props) {
const classes = useStyles();
const ref = useRef(null);
const [needScroll, setNeedScroll] = useState(false);
useEffect(() => {
if (needScroll) {
scrollToElement();
}
}, [ref]);
const [flipQuantity] = useFlipChosenProductQuantityMutation({
variables: {
input: {
productCacheId: props.product.id,
parentCacheId: props.parentCacheId,
menuItemCacheId: props.menuItemCacheId,
},
},
onError: err => {
if (process.env.NODE_ENV !== 'test') {
console.error('Error executing Flip Chosen Product Quantity Mutation', err);
Sentry.setExtras({ error: err, query: 'useFlipChosenProductQuantityMutation' });
Sentry.captureException(err);
}
},
});
const [validateProduct] = useValidateProductMutation({
variables: { productCacheId: props.menuItemCacheId },
onError: err => {
if (process.env.NODE_ENV !== 'test') {
console.error('Error executing Validate Product Mutation', err);
Sentry.setExtras({ error: err, query: 'useValidateProductMutation' });
Sentry.captureException(err);
}
},
});
const scrollToElement = useCallback(() => {
if (ref && ref.current) {
ref.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}, [ref]);
const onClickHandler = useCallback(async () => {
await flipQuantity();
if (props.product.isValid !== ProductValidationStatus.Unknown) {
validateProduct();
}
setNeedScroll(true);
}, [flipQuantity, props.product.isValid, validateProduct, scrollToElement]);
return (
<ListItem className={classes.root}>
<div ref={ref}>
<Box display='flex' alignItems='center' onClick={onClickHandler}>
<Radio
edge='start'
checked={props.product.chosenQuantity > 0}
tabIndex={-1}
inputProps={{ 'aria-labelledby': props.product.name! }}
color='primary'
size='medium'
/>
<ListItemText
className={classes.text}
primary={props.product.name}
primaryTypographyProps={{ variant: 'body2' }}
/>
<ListItemText
className={classes.price}
primary={getProductPrice(props.product)}
primaryTypographyProps={{ variant: 'body2', noWrap: true, align: 'right' }}
/>
</Box>
{props.product.chosenQuantity > 0 &&
props.product.subproducts &&
props.product.subproducts.map(subproduct => (
<ListItem component='div' className={classes.multiLevelChoosable} key={subproduct!.id}>
<Choosable
product={subproduct!}
parentCacheId={props.product.id}
menuItemCacheId={props.menuItemCacheId}
is2ndLevel={true}
/>
</ListItem>
))}
</div>
</ListItem>
);
}
Now I am getting this error:
index.js:1375 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
I've previously solved this by adding a local state flag to items that should be scrolled to when they appear:
apolloClient.mutate({
mutation: MY_MUTATE,
variables: { ... },
update: (proxy, { data: { result } }) => {
// We mark the item with the local prop `addedByThisSession` so that we know to
// scroll to it once mounted in the DOM.
apolloClient.cache.writeData({ id: `MyType:${result._id}`, data: { ... result, addedByThisSession: true } });
}
})
Then when it mounts, I force the scroll and clear the flag:
import scrollIntoView from 'scroll-into-view-if-needed';
...
const GET_ITEM = gql`
query item($id: ID!) {
item(_id: $id) {
...
addedByThisSession #client
}
}
`;
...
const MyItem = (item) => {
const apolloClient = useApolloClient();
const itemEl = useRef(null);
useEffect(() => {
// Scroll this item into view if it's just been added in this session
// (i.e. not on another browser or tab)
if (item.addedByThisSession) {
scrollIntoView(itemEl.current, {
scrollMode: 'if-needed',
behavior: 'smooth',
});
// Clear the addedByThisSession flag
apolloClient.cache.writeFragment({
id: apolloClient.cache.config.dataIdFromObject(item),
fragment: gql`
fragment addedByThisSession on MyType {
addedByThisSession
}
`,
data: {
__typename: card.__typename,
addedByThisSession: false,
},
});
}
});
...
Doing it this way means that I can completely separate the mutation from the item's rendering, and I can by sure that the scroll will only occur once the item exists in the DOM.
Related
Use case is to to open
http://localhost:3000/users/101
This is how I would like my page layout to be and Element pull in the second code snippet in [id].tsx
import CTASection from '../../components/samples/CTASection';
import { User } from "../../interfaces";
import { GetStaticProps, GetStaticPaths } from "next";
import Element from "pages/users/element";
const Element = ({ item, errors }: Props) => {
return (
<Box>
<Element />
<CTASection />
</Box>
)
}
export default Id;
export const getStaticPaths: GetStaticPaths = async () => {
// Get the paths we want to pre-render based on users
const paths = sampleUserData.map((user) => ({
params: { id: user.id.toString() },
}));
return { paths, fallback: false };
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
try {
const id = params?.id;
const item = sampleUserData.find((data) => data.id === Number(id));
return { props: { item } };
} catch (error) {
return { props: { errors: 'ERROR Loading Data' } };
}
};
However it only renders the query parameters if I insert my element.tsx page in a non-modular fashion like this:
...
return (
<Box>
<Grid gap={2}>
<Heading as="h2" fontSize={{ base: "lg", sm: "3xl" }}>
Verifikation
</Heading>
<Box
backgroundColor={colorMode === "light" ? "gray.200" : "gray.500"}
padding={4}
borderRadius={4}
>
<Box fontSize={textSize} title={`${
item ? item.name : "User Detail"
}`}>
{item && <ListDetail item={item} />}
</Box>
</Box>
</Grid>
<CTASection />
</Box>
)
...
This is the ListDetail.tsx:
import * as React from "react";
import { User } from "../../interfaces";
type ListDetailProps = {
item: User;
};
const ListDetail = ({ item: user }: ListDetailProps) => (
<div>
<h1>User: {user.name}</h1>
<p>ID: {user.id}</p>
</div>
);
export default ListDetail;
you can use gerServerSideProps
export const getServerSideProps = async ({ req, res, query, params }) => {
// your code...
// you have access to req, res, query, params, headers, cookies, and many more.
return {
props: {}
}
}
I am trying to write a test to for my component which uses the Material UI Autocomplete component. I am not sure what I am doing wrong but my test doesn't seem to be triggering the onChange of the Material UI Autocomplete component.
it('should render autocomplete and select a user', async () => {
searchContact.mockResolvedValueOnce({
data: {
value: [
{
displayName: 'Jan Travis',
userPrincipalName: 'JanT#email.uk',
},
{
displayName: 'Jon Test',
userPrincipalName: '',
},
{
displayName: 'Jay Test',
userPrincipalName: 'JayT#email.uk',
},
],
},
});
initialProps.activityName = 'some-activity';
initialProps.testId = 'contact-person[0]';
initialProps.fromType = 'planning-contact-person';
const { getByRole } = render(<AutoCompleteUserSearch {...initialProps} />);
const autocomplete = getByRole('textbox');
autocomplete.focus();
await act(async () => {
fireEvent.change(document.activeElement, { target: { value: 'Jay' } });
});
fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' });
fireEvent.keyDown(document.activeElement, { key: 'Enter' });
await waitFor(() => {
expect(autocomplete.value).toEqual('Jay');
});
Here is what my autocomplete component looks like
return (
<React.Fragment>
<Autocomplete
id={props.activityName ? props.activityName : props.id}
freeSolo
data-testid={props.testId ? props.testId : 'autocomplete'}
defaultValue=""
getOptionLabel={(option) => (typeof option === 'string' ? option : option.displayName)}
getOptionSelected={(option, value) => {
return option.displayName === value;
}}
filterOptions={(x) => x}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
value={autoCompleteValue}
autoComplete
includeInputInList
options={[...autoCompleteOptions]}
filterSelectedOptions
clearOnEscape
onChange={(event, newValue, reason) => {
setAutoCompleteOptions(newValue ? [newValue, ...autoCompleteOptions] : autoCompleteOptions);
if (newValue) {
setAutoCompleteValue(newValue.displayName);
if (newValue.userPrincipalName) {
setSelectUserEmailAddress(newValue.userPrincipalName);
setUserEmailAddressError();
} else {
setUserEmailAddressError('This user does not have an email address');
}
}
if (reason === 'clear') {
setAutoCompleteValue('');
}
}}
size="small"
renderInput={(params) => (
<div ref={params.InputProps.ref}>
<Input
type="text"
label="Search user"
name={props.activityName ? props.activityName : props.name}
{...params.inputProps}
inputProps={{ 'aria-label': 'Search user' }}
onChange={(ev) => {
onChangeHandle(ev.target.value);
}}
/>
</div>
)}
renderOption={(option, { inputValue }) => {
const matches = match(option.displayName, inputValue);
const parts = parse(option.displayName, matches);
return (
<div>
{parts.map((part, index) => (
<span key={index} style={{ fontWeight: part.highlight ? 700 : 400 }}>
{part.text}
</span>
))}
</div>
);
}}
/>
The onChange within the autocomplete doesn't seem to be trigged. Not sure if the within the test the keyDown is working properly.
Here is my onChangeHandle function within the Input onChange, which is being called
const onChangeHandle = (e) => {
setAutoCompleteValue(e);
if (e !== '') {
if (e) {
searchContact(e)
.then((res) => {
setAutoCompleteOptions(res.data.value);
})
.catch(() => {});
}
}
};
Any help would be appreciated, thanks.
I've two different prices,
when i check the checkbox for second price,
the state show and display the second price properly
but when i press the pay button of that product with second price, it will send the first price
instead of what I've checked which was second price.
so i want increment both prices separately when i checked the check box and
it sends the specific price to the payment process
import React, { useState, useEffect } from "react";
import {
getProducts,
getBraintreeClientToken,
processPayment,
createOrder
} from "./apiCore";
import { emptyCart } from "./cartHelpers";
import { isAuthenticated } from "../auth";
import { Link } from "react-router-dom";
import "braintree-web";
import DropIn from "braintree-web-drop-in-react";
import { makeStyles } from '#material-ui/core/styles';
import TextField from '#material-ui/core/TextField';
const useStyles = makeStyles((theme) => ({
root: {
'& > *': {
margin: theme.spacing(1),
width: '25ch',
},
},
}));
const Checkout = ({ products, setRun = f => f, run = undefined }) => {
const classes = useStyles();
const [data, setData] = useState({
loading: false,
success: false,
clientToken: null,
error: "",
instance: {},
address: "",
mobile:""
});
>>>>>> const [price, setPrice]= useState(() => () => getTotal())
>>>>>> const [isChecked, setIsChecked]= useState(false);
const userId = isAuthenticated() && isAuthenticated().user._id;
const token = isAuthenticated() && isAuthenticated().token;
const getToken = (userId, token) => {
getBraintreeClientToken(userId, token).then(data => {
if (data.error) {
setData({ ...data, error: data.error });
} else {
setData({ clientToken: data.clientToken });
}
});
};
useEffect(() => {
getToken(userId, token);
}, []);
const handleAddress = event => {
setData({ ...data, address: event.target.value });
};
const handleMobile = event => {
setData({ ...data, mobile: event.target.value });
};
>>>>>> const getTotal = () => {
>>>>>> return products.reduce((currentValue, nextValue) => {
>>>>>> return currentValue + nextValue.count * nextValue.price;
>>>>>>
>>>>>> }, 0);
>>>>>> };
>>>>>> const getTotal2 = () => {
>>>>>> return products.reduce((currentValue, nextValue) => {
>>>>>> return currentValue + nextValue.count * nextValue.price2;
>>>>>> }, 0);
>>>>>> };**
const showCheckout = () => {
return isAuthenticated() ? (
<div>{showDropIn()}</div>
) : (
<Link to="/signin">
<button className="btn btn-primary">Sign in to checkout</button>
</Link>
);
};
let deliveryAddress = data.address
let deliveryMobile = data.mobile
const buy = () => {
setData({ loading: true });
// send the nonce to your server
// nonce = data.instance.requestPaymentMethod()
let nonce;
let getNonce = data.instance
.requestPaymentMethod()
.then(data => {
// console.log(data);
nonce = data.nonce;
// once you have nonce (card type, card number) send nonce as 'paymentMethodNonce'
// and also total to be charged
// console.log(
// "send nonce and total to process: ",
// nonce,
// getTotal(products)
// );
>>>>>> **const paymentData = {
>>>>>> paymentMethodNonce: nonce,
>>>>>> amount: getTotal(products)
>>>>>> };**
processPayment(userId, token, paymentData)
.then(response => {
console.log(response);
// empty cart
// create order
const createOrderData = {
products: products,
transaction_id: response.transaction.id,
amount: response.transaction.amount,
address: deliveryAddress,
mobile: deliveryMobile
};
createOrder(userId, token, createOrderData)
.then(response => {
emptyCart(() => {
setRun(!run); // run useEffect in parent Cart
console.log(
"payment success and empty cart"
);
setData({
loading: false,
success: true
});
});
})
.catch(error => {
console.log(error);
setData({ loading: false });
});
})
.catch(error => {
console.log(error);
setData({ loading: false });
});
})
.catch(error => {
// console.log("dropin error: ", error);
setData({ ...data, error: error.message });
});
};
const showDropIn = () => (
<div onBlur={() => setData({ ...data, error: "" })}>
{data.clientToken !== null && products.length > 0 ? (
<div>
<div className="gorm-group mb-3">
<label className="text-muted">Delivery address:</label>
<textarea
onChange={handleAddress}
className="form-control"
value={data.address}
placeholder="Type your delivery address here..."
/>
</div>
<div className="gorm-group mb-3">
<label className="text-muted">mobile number:</label>
<form className={classes.root} noValidate autoComplete="off">
<TextField placeholder="mobile number" onChange={handleMobile} type="text" value={data.mobile} id="outlined-basic" label="mobile" variant="outlined" />
</form>
</div>
<DropIn
options={{
authorization: data.clientToken,
paypal: {
flow: "vault"
}
}}
onInstance={instance => (data.instance = instance)}
/>
<button onClick={buy} className="btn btn-success btn-block">
Pay
</button>
</div>
) : null}
</div>
);
const showError = error => (
<div
className="alert alert-danger"
style={{ display: error ? "" : "none" }}
>
{error}
</div>
);
const showSuccess = success => (
<div
className="alert alert-info"
style={{ display: success ? "" : "none" }}
>
Thanks! Your payment was successful!
</div>
);
const showLoading = loading =>
loading && <h2 className="text-danger">Loading...</h2>;
return (
<div>
>>>>>> **<form>
>>>>>> <h1>قیمت کل: {isChecked ? getTotal2() : getTotal()} </h1>
>>>>>> <label>نیم لیتر :</label>
>>>>> <input type="checkbox"
>>>>>> checked={isChecked}
>>>>>> onChange={(e)=>{setIsChecked(e.target.checked)}}/>
>>>>>> </form>**
{showLoading(data.loading)}
{showSuccess(data.success)}
{showError(data.error)}
{showCheckout()}
</div>
);
};
export default Checkout;
my card component
import React, { useState, version,useEffect } from 'react';
import { Link, Redirect } from 'react-router-dom';
import ShowImage from './ShowImage';
import moment from 'moment';
import { addItem, updateItem, removeItem } from './cartHelpers';
import logo from '../images/logo.svg'
//material ui
import Card from '#material-ui/core/Card';
import CardContent from '#material-ui/core/CardContent';
import { makeStyles } from '#material-ui/core/styles';
import Button from '#material-ui/core/Button';
import Grid from '#material-ui/core/Grid'
import { Typography } from '#material-ui/core';
import CardHeader from '#material-ui/core/CardHeader';
import CardMedia from '#material-ui/core/CardMedia';
import CardActions from '#material-ui/core/CardActions';
import Collapse from '#material-ui/core/Collapse';
import Avatar from '#material-ui/core/Avatar';
import IconButton from '#material-ui/core/IconButton';
import MoreVertIcon from '#material-ui/icons/MoreVert';
import FormControlLabel from '#material-ui/core/FormControlLabel';
import Menu from '#material-ui/core/Menu';
import MenuItem from '#material-ui/core/MenuItem';
import { create } from 'jss';
import rtl from 'jss-rtl';
import { StylesProvider, jssPreset } from '#material-ui/core/styles';
const jss = create({ plugins: [...jssPreset().plugins, rtl()] });
const useStyles = makeStyles((theme) => ({
stock: {
marginBottom: '1rem'
},
Button: {
...theme.typography.button,
padding: '.3rem',
minWidth: 10,
},
media: {
height: 0,
paddingTop: '56.25%', // 16:9
},
title: {
backgroundColor: '#D8D8D8'
},
grid: {
padding: '0'
},
cardFont:{
...theme.typography.body,
textAlign:'right',
marginTop:'.8rem',
marginRight:'1rem'
},productName:{
...theme.typography.body,
textAlign:'right',
},
cardButton:{
marginLeft:'1.4rem'
}
}));
const Cardd = ({
product,
showViewProductButton = true,
showAddToCartButton = true,
cartUpdate = false,
showRemoveProductButton = false,
setRun = f => f,
run = undefined
// changeCartSize
}) => {
const [redirect, setRedirect] = useState(false);
const [count, setCount] = useState(product.count);
//material ui
const [anchorEl, setAnchorEl] = React.useState(null);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
// console.log(price)
const classes = useStyles()
const showViewButton = showViewProductButton => {
return (
showViewProductButton && (
<Link to={`/product/${product._id}`} className="mr-2">
<Button className={classes.Button} size="small" variant="contained">مشاهده محصول</Button>
</Link>
)
);
};
const addToCart = () => {
// console.log('added');
addItem(product, setRedirect(true));
};
const shouldRedirect = redirect => {
if (redirect) {
return <Redirect to="/cart" />;
}
};
const showAddToCartBtn = showAddToCartButton => {
return (
showAddToCartButton && (
<Button className={classes.Button} size="small" onClick={addToCart} variant="contained" color="primary" disableElevation>
اضافه کردن به سبد
</Button>
)
);
};
const showStock = quantity => {
return quantity > 0 ? (
<Button disabled size="small" className={classes.stock}>موجود </Button>
) : (
<Button className={classes.stock}>ناموجود </Button>
);
};
const handleChange = productId => event => {
setRun(!run); // run useEffect in parent Cart
setCount(event.target.value < 1 ? 1 : event.target.value);
if (event.target.value >= 1) {
updateItem(productId, event.target.value);
}
};
const showCartUpdateOptions = cartUpdate => {
return (
cartUpdate && (
<div>
<div className="input-group mb-3">
<div className="input-group-prepend">
<span className="input-group-text">تعداد</span>
</div>
<input type="number" className="form-control" value={count} onChange={handleChange(product._id)} />
</div>
</div>
)
);
};
const showRemoveButton = showRemoveProductButton => {
return (
showRemoveProductButton && (
<Button className={classes.Button} size="small" variant="contained" color="secondary" disableElevation
onClick={() => {
removeItem(product._id);
setRun(!run); // run useEffect in parent Cart
}}
>
حذف محصول
</Button>
)
);
};
return (
<Grid container direction='column' >
<Grid item >
<Card style={{margin:'1rem'}}>
<Grid item className={classes.title}>
<Typography className={classes.productName} variant='h6'>
{product.name}
</Typography>
</Grid>
<CardHeader
avatar={
<Avatar alt="Remy Sharp" src={logo} className={classes.green}>
</Avatar>
}
action={
<IconButton aria-label="settings">
<MoreVertIcon />
</IconButton>
}
title="روغنک"
subheader="September 14, 2016"
/>
<CardContent className={classes.grid} >
{shouldRedirect(redirect)}
<ShowImage item={product} url="product" />
<StylesProvider jss={jss}>
<Typography className={classes.cardFont} variant="body2" component="p">
{product.description.substring(0, 100)}
</Typography>
</StylesProvider>
<div>
<Button aria-controls="simple-menu" aria-haspopup="true" onClick={handleClick}>
Open Menu
</Button>
<Menu
id="simple-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleClose}
>
<MenuItem onClick={handleClose}>Profile</MenuItem>
<MenuItem onClick={handleClose}>My account</MenuItem>
<MenuItem onClick={handleClose}>Logout</MenuItem>
</Menu>
</div>
<Typography className={classes.cardFont} variant="body2" component="p">
$یک لیتر {product.price}
</Typography>
<Typography className={classes.cardFont} variant="body2" component="p">
$نیم لیتر {product.price2}
</Typography>
<Typography className={classes.cardFont} variant="body2" component="p">
Category: {product.category && product.category.name}
</Typography>
<Typography className={classes.cardFont} variant="body2" component="p">
Added on {moment(product.createdAt).fromNow()}
</Typography>
<Typography className={classes.cardFont} variant="body2" component="p">
{showStock(product.quantity)}
</Typography>
<br />
<Grid container >
<Grid item className={classes.cardButton} >
{showViewButton(showViewProductButton)}
</Grid>
<Grid item className={classes.cardButton}>
{showAddToCartBtn(showAddToCartButton)}
</Grid>
<Grid item className={classes.cardButton}>
{showRemoveButton(showRemoveProductButton)}
</Grid>
</Grid>
{showCartUpdateOptions(cartUpdate)}
</CardContent>
</Card>
</Grid>
</Grid>
// <div className="card ">
// <div className="card-header card-header-1 ">{product.name}</div>
// <div className="card-body">
// {shouldRedirect(redirect)}
// <ShowImage item={product} url="product" />
// <p className="card-p mt-2">{product.description.substring(0, 100)} </p>
// <p className="card-p black-10">$ {product.price}</p>
// <p className="black-9">Category: {product.category && product.category.name}</p>
// <p className="black-8">Added on {moment(product.createdAt).fromNow()}</p>
// {showStock(product.quantity)}
// <br />
// {showViewButton(showViewProductButton)}
// {showAddToCartBtn(showAddToCartButton)}
// {showRemoveButton(showRemoveProductButton)}
// {showCartUpdateOptions(cartUpdate)}
// </div>
// </div>
);
};
export default Cardd;
CartHelper component
export const addItem = (item = [], count = 0, next = f => f) => {
let cart = [];
if (typeof window !== 'undefined') {
if (localStorage.getItem('cart')) {
cart = JSON.parse(localStorage.getItem('cart'));
}
cart.push({
...item,
count: 1
});
// remove duplicates
// build an Array from new Set and turn it back into array using Array.from
// so that later we can re-map it
// new set will only allow unique values in it
// so pass the ids of each object/product
// If the loop tries to add the same value again, it'll get ignored
// ...with the array of ids we got on when first map() was used
// run map() on it again and return the actual product from the cart
cart = Array.from(new Set(cart.map(p => p._id))).map(id => {
return cart.find(p => p._id === id);
});
localStorage.setItem('cart', JSON.stringify(cart));
next();
}
};
export const itemTotal = () => {
if (typeof window !== 'undefined') {
if (localStorage.getItem('cart')) {
return JSON.parse(localStorage.getItem('cart')).length;
}
}
return 0;
};
export const getCart = () => {
if (typeof window !== 'undefined') {
if (localStorage.getItem('cart')) {
return JSON.parse(localStorage.getItem('cart'));
}
}
return [];
};
export const updateItem = (productId, count) => {
let cart = [];
if (typeof window !== 'undefined') {
if (localStorage.getItem('cart')) {
cart = JSON.parse(localStorage.getItem('cart'));
}
cart.map((product, i) => {
if (product._id === productId) {
cart[i].count = count;
}
});
localStorage.setItem('cart', JSON.stringify(cart));
}
};
export const removeItem = productId => {
let cart = [];
if (typeof window !== 'undefined') {
if (localStorage.getItem('cart')) {
cart = JSON.parse(localStorage.getItem('cart'));
}
cart.map((product, i) => {
if (product._id === productId) {
cart.splice(i, 1);
}
});
localStorage.setItem('cart', JSON.stringify(cart));
}
return cart;
};
export const emptyCart = next => {
if (typeof window !== 'undefined') {
localStorage.removeItem('cart');
next();
}
};
I have this component that displays data but also needs to save and manipulate state. I have 5 other components that are exactly the same, except for little nuances inside of their functions. I need help finding a more elegant way to write this code, rather than having 6 files that are more or less the same.
I have tried to understand and use a HOC for this instance, but each component has its own submit call that requires data specific to that component. So, I can't seem to find a way to make a HOC work.
These are my functions:
componentDidMount () {
setTimeout(() => {
this.setState({ baseForm: this.props.baseData })
this.getDisable()
this.setState({ loading: false })
}, 2000)
}
handleSwitch = item => event => {
this.setState({ [item]: event.target.checked })
}
handlePRESubmit () {
const array = this.state.baseForm
array.forEach(item => {
item.isTemplate = false
item.mark = this.state.mark
item.isVisible = this.state.visible
item.genesisId = item._id
})
}
handleSubmit = () => {
this.handlePRESubmit()
let formData = this.state.baseForm[0]
fetch(APIURL, {
method: 'POST',
body: JSON.stringify(formData),
}).then(response => {
response.json().then(data => {
let orgId = localStorage.getItem('orgId')
let sku = { skuId: data.data._id, type: 'verification' }
fetch(APIURL, {})
.then(response => response.json())
.then(data => {})
})
})
}
toggleDisabled () {
if (this.state.assigned !== undefined) {
this.setState({ disabled: !this.state.disabled })
}
}
getDisable () {
setTimeout(() => {
const result = this.props.assignedSku.find(e => e.name === 'Base')
this.setState({ assigned: result })
if (this.state.assigned !== undefined) {
this.setState({ mark: true })
this.setState({ visible: true })
}
}, 1000)
}
handleMenu = event => {
this.setState({ anchorEl: event.currentTarget })
}
handleClose = () => {
this.setState({ anchorEl: null })
}
And this is my Card
<Card id='partner-sku-card'>
<CardHeader
title={base.name}
subheader={'$' + ' ' + base.price}
action={
<div>
<IconButton onClick={this.handleMenu}/>
</div>
}
/>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={this.handleClose}
>
<MenuItem disabled={this.state.disabled ? 'disabled' : ''}>
Edit
</MenuItem>
</Menu>
<CardActions>
<FormControlLabel
control={
<Switch
checked={this.state.mark}
onChange={this.handleSwitch('mark')}
value='mark'
/>
}
/>
<FormControlLabel
control={
<Switch
checked={this.state.visible}
onChange={this.handleSwitch('visible')}
value='visible'
/>
}
/>
<Button onClick={this.handleSubmit}>
Submit
</Button>
</CardActions>
</Card>
Again, all of this code is being written again in 5 other files. I need an elegant way to replace the word "Base" / "base" in every aspect of this. Lets say I have Base, Mid, Top. I would need all of these functions to work for all 3, and still produce the same card at the end.
Thanks for the help!
Create a component called CardWrapper or something. Then in each other file call CardWrapper like this:
class First extends Component {
handleSwitch = () => {
//its own implementation
}
handlePreSubmit = () => {
//its own implementation
}
handleSubmit = () => {
//its own implementation
}
//other methods that you need for each component to be different
render(){
return (
<CardWrapper handlePreSubmit={this.handlePreSubmit} handleSubmit={this.handleSubmit} handleSwitch={this.handleSwitch} />
)
}
}
Remember that you should add all of this to all the files that share the CardWrapper component.
And then in CardWrapper you can access it by this.props. Ex. in the end when you have Submit Button that would change like:
<Button onClick={this.props.handleSubmit}>
Submit
</Button>
When updating an array (inside an object), by adding an object to it, the child component is not re-rendered. The parent component is, however.
I tried updating a non-array property of the object, while also updating the array property of the object, the child component will then update. E.g:
Does not work:
obj.arr.push(user);
Works:
obj.arr.push(user);
obj.test = "wow";
My problem exists with the users prop, passed to the Users component from the Lobby component. When a user joins, the socket event lobby_player_joined is triggered, modifying the users array.
Lobby component (parent):
...
const StyledTabs = styled(Tabs)`${TabsStyle};`;
class Lobby extends Component {
constructor(props) {
super(props);
this.state = {
tab: 0,
};
this.props.setTitle('Lobby');
}
static get propTypes() {
return {
history: PropTypes.shape({ push: PropTypes.func.isRequired }).isRequired,
location: PropTypes.shape({ state: PropTypes.object }).isRequired,
setTitle: PropTypes.func.isRequired,
initializeSocket: PropTypes.func.isRequired,
onceSocketMessage: PropTypes.func.isRequired,
onSocketMessage: PropTypes.func.isRequired,
sendSocketMessage: PropTypes.func.isRequired,
};
}
async componentDidMount() {
await this.props.initializeSocket((error) => {
console.error(error);
});
await this.props.onSocketMessage('exception', (error) => {
console.log(error);
});
await this.props.onceSocketMessage('lobby_joined', (lobby) => {
this.setState({ lobby });
});
await this.props.sendSocketMessage('lobby_join', {
id: this.props.location.state.id,
password: this.props.location.state.password,
});
await this.props.onSocketMessage('lobby_player_joined', (user) => {
const { lobby } = this.state;
lobby.users.push(user);
return this.setState({ lobby });
});
await this.props.onSocketMessage('lobby_player_left', (user) => {
const { lobby } = this.state;
const userIndex = lobby.users.findIndex(u => u.id === user.id);
if (userIndex !== -1) {
lobby.users.splice(userIndex, 1);
this.setState({ lobby });
}
});
await this.props.onSocketMessage('lobby_new_host', (host) => {
const { lobby } = this.state;
lobby.host = host;
return this.setState({ lobby });
});
}
handleTab = (event, value) => {
console.log(this.state.lobby);
this.setState({ tab: value });
};
handleSwipe = (value) => {
this.setState({ tab: value });
};
render() {
if (!this.state.lobby) {
return (<div> Loading... </div>);
}
return (
<Container>
<AppBar position="static">
<StyledTabs
classes={{
indicator: 'indicator-color',
}}
value={this.state.tab}
onChange={this.handleTab}
fullWidth
centered
>
<Tab label="Users" />
<Tab label="Info" />
</StyledTabs>
</AppBar>
<SwipeableViews
style={{ height: 'calc(100% - 48px)' }}
containerStyle={{ height: '100%' }}
index={this.state.tab}
onChangeIndex={this.handleSwipe}
>
<TabContainer>
<Users
{...this.state.lobby}
/>
</TabContainer>
<TabContainer>
<Info
{...this.state.lobby}
/>
</TabContainer>
</SwipeableViews>
</Container>
);
}
}
...
Users component (child):
...
class Users extends Component {
state = {
isReady: false,
usersReady: [],
};
async componentDidMount() {
await this.props.onSocketMessage('lobby_user_ready', (data) => {
this.setState(prevState => ({
usersReady: [...prevState.usersReady, data.socketId],
}));
});
await this.props.onSocketMessage('lobby_user_unready', (data) => {
this.setState(prevState => ({
usersReady: prevState.usersReady.filter(id => id !== data.socketId),
}));
});
}
componentWillUnmount() {
this.props.offSocketMessage('lobby_user_ready');
this.props.offSocketMessage('lobby_user_unready');
}
static get propTypes() {
return {
id: PropTypes.number.isRequired,
users: PropTypes.arrayOf(PropTypes.object).isRequired,
userCount: PropTypes.number.isRequired,
host: PropTypes.shape({
username: PropTypes.string.isRequired,
}).isRequired,
sendSocketMessage: PropTypes.func.isRequired,
onSocketMessage: PropTypes.func.isRequired,
offSocketMessage: PropTypes.func.isRequired,
};
}
readyChange = () => {
this.setState(prevState => ({ isReady: !prevState.isReady }), () => {
if (this.state.isReady) {
return this.props.sendSocketMessage('lobby_user_ready', { id: this.props.id });
}
return this.props.sendSocketMessage('lobby_user_unready', { id: this.props.id });
});
};
renderStar = (user) => {
const { host } = this.props;
if (host.username === user.username) {
return (<Icon>star</Icon>);
}
return null;
}
render() {
return (
<UserContainer>
{ this.props.users.length }
<CardsContainer>
{this.props.users.map(user => (
<UserBlock
className={this.state.usersReady.includes(user.socketId) ? 'flipped' : ''}
key={user.socketId}
>
<BlockContent className="face front">
{ this.renderStar(user) }
<div>{user.username}</div>
<Icon className="icon">
close
</Icon>
</BlockContent>
<BlockContent className="face back">
<Icon>
star
</Icon>
<div>{user.username}</div>
<Icon className="icon">
check
</Icon>
</BlockContent>
</UserBlock>
))}
</CardsContainer>
<InfoContainer>
<p>Players</p>
<p>
{this.props.users.length}
{' / '}
{this.props.userCount}
</p>
<p>Ready</p>
<p>
{this.state.usersReady.length}
{' / '}
{this.props.userCount}
</p>
</InfoContainer>
<StyledButton
variant={this.state.isReady ? 'outlined' : 'contained'}
color="primary"
onClick={this.readyChange}
>
{ this.state.isReady ? 'Unready' : 'ready'}
</StyledButton>
</UserContainer>
);
}
}
...
Could anyone help me out with making the Users component update/re-render when modifying the array prop?
Don't mutate the state. Use something like this
await this.props.onSocketMessage('lobby_player_joined', (user) => {
const { lobby } = this.state;
return this.setState({ lobby : {...lobby, users: lobby.users.concat(user)} });
});
edit: fixed missing bracket
This is because React compares props for equality to determine whether to re-render a component. Instead of
obj.arr.push(user);
Try
const newObj = {...obj, arr: obj.arr.concat(user)};
which creates a new Object.
An alternative is using Immutable.js