Getting Error in testing react component even when test passed - javascript

I am writing unit test cases for my React component using Jest and Enzyme. I am receiving an error in this component's testing even when all my test cases are passing.
I am sharing my component code and the error I am getting below
class BidSummary extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: false,
techSpecMetric: [],
isStateChanged: 1
};
}
componentDidMount() {
this.fetchTechSpec();
}
fetchTechSpec = async () => {
this.setState({
isLoading: true
});
const data = {
quote_summary_id: get(this.props.match, 'params.bidId', '')
};
const techSpec = await this.props.fetchAssignedTechspec(data);
if (isSuccess(techSpec)) {
this.setState({
techSpecMetric: (get(techSpec, 'payload.data', [])),
isLoading: false
});
}
}
showmaterialIds = (cell, row) => {
const content = row.Materials.map(item => (
<li key={item.UPCProductConfigurationId}>
<span className="materialName">{item.MaterialCode}</span>
<span className="materialDescription">{item.MaterialName}</span>
</li>
));
return (
<div>
<Popover
trigger="click"
placement="bottomLeft"
content={<ul className="materialIdsList">{content}</ul>}
title={`Material IDs for ${row.TechSpecCode} (${row.Materials.length})`}
overlayClassName="xyz"
>
<div
style={{
fontSize: '11px',
textDecoration: 'underline',
color: '#32C77F',
display: 'inline-block'
}}
>
{row.Materials.length}
</div>
</Popover>
</div>
);
};
/* This function calls when user click on Cancel link in the footer */
onClickCancel = () => {
this.props.history.push('/');
}
/* This function calls when user click on Back link in the footer */
onClickBack = () => {
this.saveAsDraftGroupData('back');
}
/* This function calls when user click on Cancel, Back or Save as draft link in the footer.
It is used to save data.
*/
saveAsDraftGroupData = async (type) => {
const arrayWithGroups = [];
forEach(this.state.techSpecMetric, (data) => {
if (data.GroupName) {
let sendingData = null;
sendingData = {
QuoteSummaryID: data.QuoteSummaryID,
GroupName: data.GroupName
};
arrayWithGroups.push(sendingData);
}
});
const quoteId = get(this.props.match, 'params.bidId', '');
const response = await this.props.saveAssignedTechspec({ data: arrayWithGroups, is_draft: true, quote_summary_id: quoteId });
isSuccess(response);
if (type === 'back') {
this.props.history.push(`/bid/${quoteId}/assign-techspec`);
}
}
saveBidQuote = async () => {
this.setState({
isLoading: true
});
const data = {
QuoteSummaryID: get(this.props.match, 'params.bidId', '')
};
const response = await this.props.updatePqrStatus(data);
if (isSuccess(response)) {
this.setState({
isLoading: false
});
BcgSuccessNotification({
message: response.payload.status,
description: response.payload.message
});
this.props.history.push('/');
} else {
BcgSuccessNotification({
message: response.payload.status,
description: response.payload.message
});
}
}
fetchGroupsCount = (dataArray) => {
const groupCount = uniq(dataArray.filter(data => data.GroupName));
return groupCount.length;
}
techSpecCount = (dataArray, MG2) => {
dataArray = dataArray.filter(data => data.MG2 === MG2);
return dataArray.length;
}
render() {
const techSpecs = sortBy(this.state.techSpecMetric, el => el.GroupName);
return (
<div key="quote-add-group-page" className="testss">
{/* <QuoteHeader /> */}
<Spinner spinning={this.state.isLoading}>
<Row className="quote-add-product-page quote-summary-page">
<Col span={24}>
<Row>
<QuotePageTitle
title={this.context.intl.formatMessage({ id: 'quote.bid.summary.page.heading' })} />
</Row>
<Row className="tech-specs-list-section">
{
(
<BcgCollapse
className="data-table"
bordered={false}
expandIcon={({ isActive }) => <BcgIcon type="caret-right" theme="filled" rotate={isActive ? 90 : 0} />}
>
{
Object.entries(groupBy(techSpecs, 'MG2')).map(mg2Item => (
<BcgCollapse.Panel
header={<div className="collpase-title">{`${mg2Item[0]} (${this.fetchGroupsCount(mg2Item[1])})`}</div>}
key={mg2Item[0] + Math.random()}
className="grouped-mg2"
>
<Row className="mt-8 table-properties">
<BcgCollapse
className="data-table"
bordered={false}
expandIcon={({ isActive }) => <BcgIcon type="caret-right" theme="filled" rotate={isActive ? 90 : 0} />}
>
{
Object.entries(groupBy((mg2Item[1]), 'GroupName')).map((groupItem, index) => {
console.log('sorted array :::::::', sortBy(groupItem[1], el => el.GroupName));
return ((groupItem[1].length > 0) && (mg2Item[1].length > 0 && mg2Item[1].some(el => (el.GroupName === groupItem[0] !== null && el.GroupName === groupItem[0])))
) ? (
<BcgCollapse.Panel
header={`${groupItem[0]} (${this.techSpecCount(groupItem[1], mg2Item[0])})`}
key={index}
>
{
<BcgTable
loading={this.state.tableLoading}
columns={this.state.columns}
rowKey={record => (`${record.QuoteSummaryID}`)}
size="middle"
className="grouped-items-table"
dataSource={groupItem[1]}
pagination={false}
/>
}
</BcgCollapse.Panel>
) : (
<div style={{ paddingRight: '20px' }}>
<BcgTable
loading={this.state.tableLoading}
columns={this.state.columns}
rowKey={record => (`${record.QuoteSummaryID}`)}
size="middle"
dataSource={groupItem[1]}
pagination={false}
/>
</div>
);
})
}
</BcgCollapse>
</Row>
</BcgCollapse.Panel>
))
}
</BcgCollapse>
)
}
</Row>
</Col>
</Row>
</Spinner>
<QuoteFooter
isStateChanged={this.state.isStateChanged}
onClickCancel={this.onClickCancel}
onClickBack={this.onClickBack}
actionProps={{
onProceed: this.saveBidQuote,
onSaveAsDraft: this.saveAsDraftGroupData
}}
showBack />
</div>
);
}
}
/**
* #readonly
*/
BidSummary.propTypes = {
match: PropTypes.object,
saveAssignedTechspec: PropTypes.func,
fetchAssignedTechspec: PropTypes.func,
history: typeOfObject,
getQuoteStatus: PropTypes.func,
updatePqrStatus: PropTypes.func
};
/**
* #readonly
*/
BidSummary.contextTypes = {
intl: PropTypes.object
};
function mapDispatchToProps(dispatch) {
return {
fetchAssignedTechspec: data => dispatch(fetchAssignedTechspec(data)),
saveAssignedTechspec: data => dispatch(saveAssignedTechspec(data)),
updatePqrStatus: data => dispatch(updatePqrStatus(data))
};
}
export {BidSummary};
export default connect('', mapDispatchToProps)(withRouter(BidSummary));
Now below is the test I am running for this component
describe('BidSummary', () => {
const match = {
params: {
bidId: 'asdasdd'
}
};
it('should render correctly', () => {
const wrapper = shallowWithIntl(<BidSummary match={match} history={undefined}/>);
expect(wrapper).toMatchSnapshot();
});
it('should contains a class', () => {
const wrapper = shallowWithIntl(<BidSummary match={match} history={undefined}/>);
expect(wrapper.exists('.quote-summary-page')).toEqual(true);
});
it('should contains a spinner', () => {
const wrapper = shallowWithIntl(<BidSummary match={match} history={undefined}/>);
expect(wrapper.exists(Spinner)).toEqual(true);
});
it('should contains Collapse component', () => {
const wrapper = shallowWithIntl(<BidSummary match={match} history={undefined}/>);
expect(wrapper.find(BcgCollapse)).toHaveLength(1);
});
it('should fetch data from api', () => {
const getData = {
quote_summary_id: ''
};
let fetchAssignedTechspec = jest.fn();
const wrapper = shallowWithIntl(<BidSummary fetchAssignedTechspec={fetchAssignedTechspec} />);
expect(wrapper).toBeDefined();
expect(fetchAssignedTechspec).toHaveBeenCalled();
expect(fetchAssignedTechspec.mock.calls[0]).toEqual([getData]);
});
it('should fetch data from api', () => {
const getData = {
quote_summary_id: ''
};
let fetchAssignedTechspec = jest.fn();
const wrapper = shallowWithIntl(<BidSummary fetchAssignedTechspec={fetchAssignedTechspec} />);
expect(wrapper).toBeDefined();
expect(fetchAssignedTechspec).toHaveBeenCalled();
expect(fetchAssignedTechspec.mock.calls[0]).toEqual([getData]);
});
});
Now these tests are getting passed but I am also receiving the error mentioned below
UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 69): TypeError: _this.props.fetchAssignedTechspec is not a function
(node:196) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 70): TypeError: _this.props.fetchAssignedTechspec is not a function
(node:196) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 71): TypeError: _this.props.fetchAssignedTechspec is not a function
(node:196) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 72): TypeError: _this.props.fetchAssignedTechspec is not a function
(node:196) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 73): TypeError: Cannot use 'in' operator to search for 'error' in undefined
I want to know where did I do the mistake and how to write the test case correctly so I don't get this error?
Please let me know thanks.

Related

How to update state in onSubmit form & send data through components?

I have a state which I need to update with the ID returned from an endpoint call so I can call another another endpoint using that ID, I've made a state in the parent component and I use it in my first form to set the ID. I pass that id as a prop to the component that needs it but when I console.log the state, it doesn't change.
How can I pass the ID through the components?
I've added comments on the main places to look at
Here is my first form where I need the ID from -
const AddWebAppTypeForm = (props: any, ref: any) => {
const { setWebAppTypes, setNewAppValues}: AddWebAppTypeFormProps =
props;
const {
handleSubmit,
control,
reset,
formState: { isDirty },
} = useForm();
const onSubmit = (data: any) => {
let formData = new FormData();
formData.append("name", data.Title);
formData.append("description", data.Description);
formData.append("uploaded_file", data.Image[0]);
if (isDirty) {
createWebAppType(formData);
}
};
const createWebAppType = async (body: any) => {
await fetch(`${process.env.REACT_APP_API_URL}/webapptype`, {
method: "POST",
body: body,
})
.then((response) => response.json())
.then((data: IWebAppType) => {
const model: IWebAppType = {
id: data.id,
name: data.name,
image: data.image,
description: data.description,
image_url: data.image_url,
};
setNewAppValues(model.id); // <--- Set the state here
setWebAppTypes((prev) =>
prev.map((item) => (item.id === 0 ? model : item))
);
enqueueSnackbar(`Created App Succesfully`, {
variant: "success",
});
});
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<button hidden={true} ref={ref} type="submit" />
</form>
);
};
export default forwardRef(AddWebAppTypeForm);
My parent component with the states -
function WebAppTypeAccordion({ a, setWebAppTypes }: WebAppTypeAccordionProps) {
const [formEl, setFormEl] = useState(null);
const [addFormEl, setAddFormEl] = useState(null);
const [newAppValues, setNewAppValues] = useState<number>(0); // <--- state with 0 as initialised value
const handleRef = (el: any) => {
if (el !== null) {
setFormEl(el);
}
};
const handleAddRef = (el: any) => {
if (el !== null) {
setAddFormEl(el);
}
};
return (
<Accordion defaultExpanded={a.id === 0}>
<AccordionSummary
// onClick={(e) => handleOnClick(a, e)}
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1a-content"
id="panel1a-header"
>
<Typography>{a.name}</Typography>
</AccordionSummary>
<AccordionDetails>
{a.id === 0 ? (
<AddWebAppTypeForm
setWebAppTypes={setWebAppTypes}
ref={handleAddRef}
setNewAppValues={setNewAppValues} // <--- Passing setState to set id
/>
) : (
null
)}
<MappedAccordion
waobj={a}
key={a.id}
setWebAppTypes={setWebAppTypes}
editRef={formEl}
addRef={addFormEl}
newAppValues={newAppValues} // <--- Passing ID
/>
</AccordionDetails>
</Accordion>
);
}
export default WebAppTypeAccordion;
Here is where I am trying to use the ID to call another endpoint
function MappedAccordion({
waobj,
setWebAppTypes,
editRef,
addRef,
newAppValues,
}: MappedAccordionProps) {
const handleCreate = (data: FieldT) => {
let wtype: string = String(waobj.id);
if (addRef !== null) {
if (newAppValues !== 0) {
wtype = String(newAppValues); // <--- Try to use the ID but get default value
createFetch(data, wtype); // <--- Try to use the ID but get default value
}
}
createFetch(data, wtype);
};
const createFetch = (data: FieldT, wtype: string) => {
let formData = new FormData();
formData.append("name", data.name);
formData.append("link", data.link);
formData.append("wtype", wtype);
fetch(`${process.env.REACT_APP_API_URL}/webapp/`, {
method: "POST",
body: formData,
})
.then((response) => {
if (!response.ok) {
let err = new Error("HTTP status code: " + response.status);
enqueueSnackbar(`Environment already exists`, {
variant: "error",
});
throw err;
}
return response.json();
})
.then((data: IWebApp) => {
const model: FieldT = {
wid: data.id,
name: data.name,
link: data.link,
};
enqueueSnackbar(`Created Environment ${model.wid}`, {
variant: "success",
});
});
};
const onSubmit = (data: FormFields) => {
if (addRef !== null) addRef?.click(); // <--- Submit AddWebAppTypeForm form, set the ID
else editRef?.click();
let onSubmitData: FieldT[] = data.myFieldValues;
onSubmitData.map((a: FieldT, index) => {
let originalField: FieldT = initialFields[index];
if (a.wid === undefined) {
handleCreate(a);
} else {
if (JSON.stringify(a) !== JSON.stringify(originalField)) {
handleEdit(a);
}
}
});
};
return (
<div>
<form onSubmit={handleSubmit(onSubmit)} id="environment-form">
<div style={{ paddingTop: 10 }}>
<Button
type="submit" // <--- Submit form
variant="outlined"
size="small"
sx={{ marginRight: 1 }}
>
Save
</Button>
<Button
variant="outlined"
size="small"
onClick={handleAppDelete}
disabled={waobj.id === 0 ? true : false}
>
Delete
</Button>
</div>
</form>
</div>
);
}
export default MappedAccordion;
Thanks for taking a look, I appreciate any help!

Loading data into grid on click returns Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'dataState')

I have a grid created using a React library that I want to fill with data with an API call once the user clicks on a button called Fetch Products. Currently, my grid does not get populated and I get this error when I debug it:
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'dataState')
and I am not sure why. Why isn't the grid populating properly and what else can I do? Here is my code:
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Grid, GridColumn as Column } from '#progress/kendo-react-grid';
import { process } from '#progress/kendo-data-query';
import {
setExpandedState,
setGroupIds,
} from '#progress/kendo-react-data-tools';
const initialDataState = {
take: 10,
skip: 0,
products: [],
group: [
{
field: 'id',
},
],
};
const processWithGroups = (data, dataState) => {
const newDataState = process(data, dataState);
setGroupIds({
data: newDataState.data,
group: dataState.group,
});
return newDataState;
};
const fetchAllData = () => {
fetch(
'https://otp.metroservices.io/otp/routers/default/index/routes/uscalacmtarail:801/stops'
)
.then((response) => response.json())
.then((productsList) => {
const newDataState = processWithGroups(
productsList,
this.state.dataState
);
this.setState({
products: productsList, // update the data
result: newDataState, // update the procesed data
});
});
};
const FirstButton = () => {
return (
<div>
<button type="button" onClick={fetchAllData}>
Fetch Products
</button>
</div>
);
};
class App extends React.PureComponent {
state = {
dataState: initialDataState,
result: processWithGroups(initialDataState.products, initialDataState),
collapsedState: [],
products: [],
};
dataStateChange = (event) => {
const newDataState = processWithGroups(
this.state.products, // use the none processed data
event.dataState
);
this.setState({
result: newDataState,
dataState: event.dataState,
});
};
expandChange = (event) => {
const item = event.dataItem;
if (item.groupId) {
const newCollapsedIds = !event.value
? [...this.state.collapsedState, item.groupId]
: this.state.collapsedState.filter(
(groupId) => groupId !== item.groupId
);
this.setState({
collapsedState: newCollapsedIds,
});
}
};
// componentDidMount() {
// this.fetchAllData()
// }
render() {
const newData = setExpandedState({
data: this.state.result.data, // pass the proccessed data
collapsedIds: this.state.collapsedState,
});
return (
<div>
<FirstButton />
<Grid
style={{
height: '520px',
}}
resizable={true}
reorderable={true}
filterable={true}
sortable={true}
groupable={true}
data={newData}
onDataStateChange={this.dataStateChange}
{...this.state.dataState}
onExpandChange={this.expandChange}
expandField="expanded"
>
<Column field="id" filterable={false} title="ID" width="50px" />
<Column field="name" title="Name" />
<Column field="cluster" title="Cluster" filter="numeric" />
</Grid>
</div>
);
}
}
ReactDOM.render(<App />, document.querySelector('my-app'));
this.state.dataState does not exist in fetchAllData.
You need to pass the this.state.dataState in App to FirstButton then to fetchAllData. After all that, you can use that
Put the functions inside the React.Component that require to interact with states and if needed to use something to the outside of the React.Component, pass the reference state. In your code, you can put your 'fetchAllData' function inside you React.Component and pass this as a prop to your FirstButton
Example:
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Grid, GridColumn as Column } from '#progress/kendo-react-grid';
import { process } from '#progress/kendo-data-query';
import {
setExpandedState,
setGroupIds,
} from '#progress/kendo-react-data-tools';
const initialDataState = {
take: 10,
skip: 0,
products: [],
group: [
{
field: 'id',
},
],
};
const processWithGroups = (data, dataState) => {
const newDataState = process(data, dataState);
setGroupIds({
data: newDataState.data,
group: dataState.group,
});
return newDataState;
};
// pass the fetchAllData function as a prop
const FirstButton = ({fetchData}) => {
return (
<div>
<button type="button" onClick={fetchData}>
Fetch Products
</button>
</div>
);
};
class App extends React.PureComponent {
state = {
dataState: initialDataState,
result: processWithGroups(initialDataState.products, initialDataState),
collapsedState: [],
products: [],
};
dataStateChange = (event) => {
const newDataState = processWithGroups(
this.state.products, // use the none processed data
event.dataState
);
this.setState({
result: newDataState,
dataState: event.dataState,
});
};
expandChange = (event) => {
const item = event.dataItem;
if (item.groupId) {
const newCollapsedIds = !event.value
? [...this.state.collapsedState, item.groupId]
: this.state.collapsedState.filter(
(groupId) => groupId !== item.groupId
);
this.setState({
collapsedState: newCollapsedIds,
});
}
};
// you can put this function inside
fetchAllData = () => {
fetch(
'https://otp.metroservices.io/otp/routers/default/index/routes/uscalacmtarail:801/stops'
)
.then((response) => response.json())
.then((productsList) => {
const newDataState = processWithGroups(
productsList,
this.state.dataState
);
this.setState({
products: productsList, // update the data
result: newDataState, // update the procesed data
});
});
};
// componentDidMount() {
// this.fetchAllData()
// }
render() {
const newData = setExpandedState({
data: this.state.result.data, // pass the proccessed data
collapsedIds: this.state.collapsedState,
});
return (
<div>
<FirstButton fetchData={this.fetchAllData}/>
<Grid
style={{
height: '520px',
}}
resizable={true}
reorderable={true}
filterable={true}
sortable={true}
groupable={true}
data={newData}
onDataStateChange={this.dataStateChange}
{...this.state.dataState}
onExpandChange={this.expandChange}
expandField="expanded"
>
<Column field="id" filterable={false} title="ID" width="50px" />
<Column field="name" title="Name" />
<Column field="cluster" title="Cluster" filter="numeric" />
</Grid>
</div>
);
}
}
ReactDOM.render(<App />, document.querySelector('my-app'));

React scroll to element after render

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.

Child component not updating after array prop updated

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

React updates the component but not the actual HTML weird behavior

I am experiencing a very weird react behavior. There is this component which gets message and emailSubmitedText. In render method based on some condition it should render either first or the other one.
Now at first it is message. I click on the submit of the form. All the functions happen.
The component rerenders. In the console log I can see this time it should render emailSubmitedText. In react devtools it show the right text.
However in the actual html and html inspector it still shows the previos text.
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Details from './Details'
class DefaultMessage extends Component {
inputRef = null
renderEmailForm = () => {
return (
<form
className='b2c_email-form input-field'
onSubmit={e => {
e.preventDefault()
const { projectId, visitSessionId } = this.props
this.setState({ email: this.inputRef.value })
this.props.onSubmitEmail({
email: this.inputRef.value,
convertedPage: window.location.href, projectId, visitSessionId
})
}}
>
<div className="input-field">
<input ref={elem => this.inputRef = elem} id='email' type='email' className='validate' value={this.props.email} />
<label htmlFor='email' data-error='Invalid email address'>E-mail</label>
</div>
<button
className='b2c_email-form-button waves-effect waves-light btn'
type='submit'
style={{
backgroundColor: this.props.companyColor || '#63bc78'
}}
>Submit</button>
</form>
)
}
render = () => {
console.log('...> ', this.props.error || !this.props.contactId && this.props.message || this.props.emailSubmitedText)
return (
<div className='b2c_chat-message'>
<Details
classNames='b2c_chat-message-details__admin'
avatar={this.props.avatar}
name={this.props.name}
date={this.props.date}
/>
<div className='b2c_chat-message-text b2c_chat-message-text__admin b2c_chat-message-default'>
<div className='b2c_chat-message-after b2c_chat-message-after__admin' />
{this.props.error || !this.props.contactId && this.props.message || this.props.emailSubmitedText}
{!this.props.contactId && this.renderEmailForm()}
</div>
</div>
)
}
}
DefaultMessage.propTypes = {
projectId: PropTypes.string.isRequired,
visitSessionId: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
date: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
onSubmitEmail: PropTypes.func.isRequired
}
export default DefaultMessage
Here is the direct parent of the component.
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import $ from 'jquery'
import moment from 'moment'
import randomstring from 'randomstring'
import DefaultMessage from './DefaultMessage'
import Message from './Message'
import UserTypingIndicator from '../UserTypingIndicator'
import TypingIndicator from './TypingIndicator'
class Messages extends Component {
chatRef = null
componentDidUpdate () {
this.scrollToTheLastMessage()
}
scrollToTheLastMessage = () => {
const $chat = $(this.chatRef)
const scrollTop = $chat.prop('scrollHeight') - $chat.innerHeight()
$chat.scrollTop(scrollTop)
}
renderDefaultMessage = () => (
<DefaultMessage
contactId={this.props.contactId}
companyColor={this.props.companyColor}
error={this.props.error}
date={moment().format('h:mm A')}
name={this.props.adminName}
avatar={this.props.adminAvatar}
message={this.props.welcomeMessage}
emailSubmitedText={this.props.emailSubmitedText}
projectId={this.props.projectId}
visitSessionId={this.props.visitSessionId}
onSubmitEmail={this.props.onSubmitEmail}
/>
)
renderMessages = () => {
let checkConversationDate = null
const {messages, contactName, adminName, adminAvatar} = this.props
const compareConversationDates = (createdAt) => {
checkConversationDate = moment(createdAt).format("DD.MM.YYYY")
return (
<div key={randomstring.generate()} className='conversationDayDate'>
<span>{checkConversationDate}</span>
</div>
)
}
if (!messages) return null
return messages.map((message, index) => {
return (
<div>
{checkConversationDate !== moment(message.createdAt.$date).format("DD.MM.YYYY") ? compareConversationDates(message.createdAt.$date) : ''}
{/* {index === 0 ? this.renderDefaultMessage() : ''} */}
<Message
isAdmin={message.userId ? true : false}
imageFile={message.imageFile}
key={randomstring.generate()}
companyColor={this.props.companyColor}
contactName={contactName}
adminName={adminName}
avatar={adminAvatar}
message={message.message}
date={moment(message.createdAt.$date).format('h:mm A')}
/>
</div>
)
})
}
renderTypingIndicators = () => {
const arrayToRender = [
this.props.isAdminTyping && <AdminTypingIndicator />,
this.props.isUserTyping && <UserTypingIndicator />
]
return arrayToRender
}
render = () => <div ref={elem => this.chatRef = elem} id='chat' className='chat-body' style={{
height: 'calc(100% - 190px - 3rem)',
overflowY: 'scroll',
margin: '30px 10px 10px 0',
boxSizing: 'border-box'
}}>
{this.renderDefaultMessage()}
{this.renderMessages()}
{this.renderTypingIndicators()}
</div>
}
Messages.propTypes = {
projectId: PropTypes.string.isRequired,
visitSessionId: PropTypes.string.isRequired,
messages: PropTypes.array.isRequired,
adminName: PropTypes.string.isRequired,
contactName: PropTypes.string.isRequired,
onSubmitEmail: PropTypes.func.isRequired
}
export default Messages
And here is where Container with states
import React, { Component } from 'react'
import Sound from 'react-sound'
import ddp from '../../ddp'
import Cookies from 'js-cookie'
import randomstring from 'randomstring'
import ChatContainer from './ChatContainer'
import Icon from './Icon'
import { connect, makeArrayCollectionFromObjectCollection, getNewMessages } from '../../functions'
class View extends Component {
defaultDocumentTitle = null
state = {
contactId: '',
chat: null,
show: false,
newMessagesCount: null,
notStatus: 'STOPPED'
}
newMessageNotification = newMessages => {
if (newMessages.length && newMessages.length > this.state.newMessagesCount) {
this.setState({ notStatus: 'PLAYING' })
document.title = `(${newMessages.length}) ${this.defaultDocumentTitle}`
} else if (!newMessages.length) {
document.title = this.defaultDocumentTitle
}
if (this.state.newMessagesCount !== newMessages.length) {
this.setState({ newMessagesCount: newMessages.length })
}
}
componentWillMount () {
this.defaultDocumentTitle = document.title
}
componentDidMount = async () => {
this.setContactIdFromCookies()
await connect(ddp)
}
setContactIdFromCookies = () => {
window.Cookies = Cookies
console.warn('setContactIdFromCookies')
const contactId = Cookies.get('b2cContactId')
console.log('contactId', contactId)
if (contactId) this.setState({contactId})
}
componentDidUpdate () {
console.warn('componentDidUpdate', this.props)
if (this.state.contactId && !this.state.chat) {
this.getChat(this.state.contactId)
}
if (this.state.chat && this.state.chat.length) {
let newMessages = getNewMessages(this.state.chat)
this.newMessageNotification(newMessages)
}
}
componentWillReceiveProps = (nextProps) => {
console.warn('componentWillReceiveProps', nextProps)
if (!nextProps.contactId) return
if (this.state.chat == null) this.getChat(nextProps.contactId)
}
getChat = async (contactId) => {
console.log('getChat', contactId)
await ddp.subscribe('Messages', {contactId})
const messagesColl = ddp.getCollection('Messages')
console.log('messagesColl', messagesColl)
this.setState({chat: this.getMessages(messagesColl)})
ddp.watch('Messages', (changedDoc, message) => {
console.log('Messages collection item changed', changedDoc, message)
const messagesColl = ddp.getCollection('Messages')
this.setState({chat: this.getMessages(messagesColl)})
})
}
getMessages = collection => {
let messages = []
if (collection) {
messages = makeArrayCollectionFromObjectCollection(collection)
}
console.log('messages', messages)
return messages
}
submitEmail = ({ email, convertedPage, projectId, visitSessionId }) => ddp.call('chat.init', { email, convertedPage, projectId, visitSessionId })
.then(contactId => {
Cookies.set('b2cContactId', contactId, { expires: 90 })
this.setState({ contactId, error: '' })
})
.catch(error => {
console.error('Error >', error)
})
readMessages = () => ddp.call('readMessages', {contactId: this.state.contactId, userId: !null})
.then(res => {
console.log('res', res)
})
.catch(error => {
console.error('Error', error)
})
submitMessage = ({message, visitSessionId, imageFile}) => ddp.call('chat.submitContactMessage', { message, visitSessionId, contactId: this.state.contactId, projectId: this.props.projectId, imageFile: imageFile || null})
.then((res) => {
console.log('res', res)
})
.catch(error => {
console.error('Error', error)
this.setState({error})
})
toggleChat = () => this.setState((state) => ({show: !state.show}))
sendFileToServer = (base64File, resolve, reject) => {
ddp.call('uploadToDropbox', base64File)
.then((res) => {
this.submitMessage({message: '', visitSessionId: this.props.visitSessionId, imageFile: res})
console.log('res', res)
})
.catch(error => {
console.error('Error', error)
})
}
getBase64 = (file, resolve, reject) => {
const self = this
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = function () {
self.sendFileToServer(reader.result, resolve, reject)
}
reader.onerror = function (error) {
console.error('FileReader Error: ', error)
}
}
onFileDrop = files => {
let self = this
files.forEach(file => {
return new Promise((resolve, reject) => {
self.getBase64(file, resolve, reject)
})
})
}
render () {
return (
<div>
<ChatContainer
onFileDrop={this.onFileDrop}
contactId={this.state.contactId}
show={this.state.show}
error={this.state.error && <span style={{color: 'red'}}>{this.state.error}</span>}
chatSettings={this.props.chatSettings}
projectId={this.props.projectId}
visitSessionId={this.props.visitSessionId}
defaultAdminUserName='default defaultAdminUserName'
contactName='You'
supportName='Our Support'
messages={this.state.chat}
onSend={this.submitMessage}
onSubmitEmail={this.submitEmail}
toggleChat={this.toggleChat}
readMessages={this.readMessages}
/>
<Icon
companyColor={this.props.chatSettings.companyColor}
onClick={this.toggleChat}
newMessagesCount={this.state.newMessagesCount}
/>
<Sound
url='https://www.incredo.co/hubfs/b2c/Not%201.wav'
playStatus={this.state.notStatus}
playFromPosition={0}
onFinishedPlaying={() => this.setState({ notStatus: 'STOPPED' })}
/>
</div>
)
}
}
export default View

Categories