I'm extremely new to ReactJS. Currently I was ask to handle a project that my previous colleague did, which he left and no one knows how exactly it work and I can't ask anyone.
Anyway, I was asked to add a confirm dialog when user attempt to redirect but (s)he changed some values on the form.
The structure of the page is kinda complicated. AFAIK, it is an parent with different children and each children has their own form. Parent itself doesn't have one. I managed to find out each children used a varaible 'pristine'(PropType.bool.isRequired) to determine if the form has been changed, and I also managed to retrieve the value of 'pristine' back to parent.
My Current Approach: Whenever a child's form has changed, it will call a function from parent, which will use setState to save the value of 'pristine'. When the user redirects, I noticed that it will trigger componentWillUnmount, so I planned to do the confirm dialog here. If the value in state is true, it will prompt the confirm dialog.
My Problem: The web browser's console keeps giving a warning:
Warning: setState(...): Cannot update during an existing state transition (such as within
render
or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved tocomponentWillMount
.
Plus, the value in state is always one step behind.
From what I have been "scavenging" for the past two day, Since the child is in 'render', doing setState in 'render' will cause inifinite loop of 'render', which is not a good approach.
Here is the code related:
index.jsx
class PageEditProperty extends React.PureComponent {
constructor(props){
super(props);
this.SetCredentialChanged = this.SetCredentialChanged.bind(this);
this.onUnload = this.onUnload.bind(this);
}
state = {
deleteModalOpen: false,
credentialsChanged: false,
};
openDeleteModal = () => {
this.setState({ deleteModalOpen: true });
};
closeDeleteModal = () => {
this.setState({ deleteModalOpen: false });
};
SetCredentialChanged = (pristine) =>{
this.setState({credentialsChanged: pristine},()=>{console.log(this.state.credentialsChanged);});
};
onUnload(){
//console.log("Redirect");
if(this.state.credentialsChanged){
confirm("Hello");
}
}
componentDidMount(){
window.addEventListener("beforeunload", this.onUnload);
//this.setState({credentialsChanged: false});
console.log("Mount");
}
componentWillUnmount(){
window.removeEventListener("beforeunload", this.onUnload);
console.log("Unmount");
console.log(this.state.credentialsChanged);
}
render() {
const childProps = {
propertyId: this.props.propertyId,
propertyExists: this.props.propertyExists,
clientPropertyId: this.props.clientPropertyId,
beforeSubmit: this.props.createPropertyIfNeeded,
setPendingRequest: this.props.setPendingRequest,
};
return (
<div className="edit-property container">
<div className="mdc-layout-grid">
<div className="mdc-layout-grid__inner">
<div className="mdc-layout-grid__cell--span-12">
<PageHeader {...childProps} onDeleteClick={this.openDeleteModal} />
</div>
<div className="mdc-layout-grid__cell--span-6 mdc-layout-grid__cell--span-6-phone mdc-layout-grid__cell--span-8-tablet">
<div className="mdc-elevation--z1 mdc-theme--background">
<ThumbnailUploader {...childProps} />
</div>
</div>
<div className="mdc-layout-grid__cell--span-6 mdc-layout-grid__cell--span-8-tablet">
<div className="mdc-elevation--z1 mdc-theme--background">
<PropertyInfoEdit {...childProps} onChangedCredentials={this.SetCredentialChanged}/>
</div>
</div>
<div className="mdc-layout-grid__cell--span-4 mdc-layout-grid__cell--span-8-tablet">
<div className="mdc-elevation--z1 mdc-theme--background">
<TripodUploader {...childProps} />
</div>
</div>
<div className="mdc-layout-grid__cell--span-8 mdc-layout-grid__cell--span-8-tablet">
<div className="mdc-elevation--z1 mdc-theme--background">
<PropertyLocationEdit {...childProps} />
</div>
</div>
<div className="mdc-layout-grid__cell--span-12 mdc-layout-grid__cell--span-8-tablet">
<div className="mdc-elevation--z1 mdc-theme--background">
<ScenesListEdit {...childProps} />
</div>
</div>
</div>
</div>
<Dialog
open={this.state.deleteModalOpen}
acceptLabel={this.props.t('Delete this content')}
cancelLabel={this.props.t('Cancel')}
acceptClassname="danger-bg"
onAccept={this.props.deleteProperty}
onClose={this.closeDeleteModal}
title={this.props.t('Delete this content ?')}
>
{this.props.t('property-delete-text')}
</Dialog>
</div>
);
}
}
PropertyInfoEdit.jsx:
PropertyInfoEdit.propTypes = {
pristine: PropTypes.bool.isRequired,
submitting: PropTypes.bool.isRequired,
valid: PropTypes.bool.isRequired,
reset: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired,
onChangedCredentials: PropTypes.func,
};
PropertyInfoEdit.defaultTypes = {
pending: false,
};
function PropertyInfoEdit({
onSubmit,
handleSubmit,
pristine,
reset,
submitting,
valid,
pending,
categories,
t,
onChangedCredentials,
}) {
function ChangeCredentials(e){
{onChangedCredentials(e)};
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="edit-property__form" onChange={onChangedCredentials(pristine)}>
<div className="mdc-card__primary form-group">
<Field name="nameEn" label={t('English name')} component={renderTextField} type="text"/>
<Field name="nameCh" label={t('Chinese name')} component={renderTextField} type="text"/>
<Field
name="categoryId"
label={t('Category')}
component={renderSelectField}
fullWidth
metaText={false}
options={categories.map(c => ({
value: c.id,
name:
t('_locale') === 'zh-CN'
? c.nameZhCN
: t('_locale') === 'zh-TW' ? c.nameZhTW : c.nameEn,
}))}
/>
</div>
<div className="mdc-card__actions" style={{ justifyContent: 'center' }}>
<div className="inline-button-group">
<RaisedButton
type="button"
onClick={reset}
disabled={pristine || submitting}
className="margin-auto mdc-button--secondary"
>
{t('Reset')}
</RaisedButton>
<RaisedButton
disabled={pristine || submitting || !valid}
type="submit"
className="margin-auto mdc-button--primary"
>
{(pending && <i className="material-icons spin2s">cached</i>) || t('Submit')}
</RaisedButton>
</div>
</div>
</form>
);
}
// region HOC Declarations
/**
* Injects the property basic information data in the component props
* @type {ComponentDecorator}
*/
const injectInfoData = injectQuery(
gql`
query PropertyInfoQuery($id: String) {
property(id: $id) {
id
nameCh
nameEn
categoryId
}
}
`,
{
options: ({ propertyId, propertyExists }) => ({
variables: { id: propertyId },
fetchPolicy: propertyExists ? 'network-only' : 'cache-only',
}),
props: ({ data }) => ({
property: data.property,
}),
},
);
/**
* Injects mutation handler to change the basic informations data
*/
const PropertyInfoUpdateMutation = gql`
mutation PropertyInfoUpdate($id: String!, $property: PropertyUpdate) {
updateProperty(id: $id, property: $property, autoPublish: false) {
id
nameCh
nameEn
categoryId
published
isValid
}
}
`;
const injectInfoMutation = injectQuery(PropertyInfoUpdateMutation, {
props: ({ ownProps, mutate }) => ({
updateDBProperty: property =>
mutate({
variables: { id: ownProps.propertyId, property },
}),
}),
});
/**
* Injects the categories
* @type {ComponentDecorator}
*/
const injectCategories = injectQuery(
gql`
query PagePropertyEditCategoryQuery {
categories(noFilter: true) {
id
nameEn
nameZhCN
nameZhTW
}
}
`,
{
props: ({ data }) => ({
categories: data.categories || [],
}),
},
);
const injectSubmitHandler = withBoundHandlers({
onSubmit(data) {
return this.props
.beforeSubmit()
.then(() => this.props.propertyExistsPromise)
.then(() => this.props.updateDBProperty(data));
},
});
/**
* Create and wraps a reduxForm around our component
* @type {function}
*/
const wrapsWithReduxForm = compose(
withProps(props => ({
form: `PropertyInfoForm#${props.clientPropertyId}`,
initialValues: props.property
? {
nameEn: props.property.nameEn || '',
nameCh: props.property.nameCh || '',
categoryId: props.property.categoryId,
}
: undefined,
})),
reduxForm({
enableReinitialize: true,
keepDirtyOnReinitialize: true,
destroyOnUnmount: false,
}),
);
// endregion HOC Declarations
// @type {React.Component}
const DecoratedPropertyInfoEdit = compose(
injectInfoData,
injectCategories,
injectInfoMutation,
wrapsWithReduxForm,
makePromiseFor(
// 1) the property must exist on the server and
// 2) it must have loaded in this component (ex we have its id)
props => props.propertyExists && props.property && props.property.id,
promise => ({ propertyExistsPromise: promise }),
),
injectSubmitHandler,
translate(),
)(PropertyInfoEdit);
DecoratedPropertyInfoEdit.propTypes = {
propertyId: PropTypes.string,
clientPropertyId: PropTypes.string.isRequired,
propertyExists: PropTypes.bool,
beforeSubmit: PropTypes.func.isRequired,
onChangedCredentials: PropTypes.func,
};
export default DecoratedPropertyInfoEdit;
I understand what I did wrong in here, but I have no idea how to fix it. So if there is anyone that can lend me a hand, that will be great because I have almost 0 knowledge with React/Redux.
Thank you so much for your help.