Je suis relativement débutant dans React, et dans cette situation particulière, il me manque probablement quelque chose de très fondamental.
Ici, j'ai une application CRUD simple, et après que l'utilisateur ajoute de nouvelles données, la liste mise à jour des éléments devrait être rendu. Et les nouvelles données sont ajoutées avec un deuxième composant Dialog AddNewDevelopmentWork.js convenient
Ainsi, après l'ajout de nouvelles données par le AddNewDevelopmentWork.js (qui est le composant enfant qui n'ouvrira qu'une boîte de dialogue pour que l'utilisateur saisisse et remplisse quelques TestFields), dans le composant principal ( DevelopmentList.js ), j'utilise componentDidUpdate pour faire la comparaison avec l'état actuel et prevState (pour la variable d'état allDevelopmentWorks qui est un tableau d'objets), et s'ils ne sont pas égaux, faites une requête à l'API Express backend et récupérez les données et mettez à jour l'état dans componentDidUpdate . Et puis restituer avec de nouvelles données.
Le problème est que ce composant principal DevelopmentList.js ne rend pas les nouvelles données saisies par l'utilisateur tant que la page n'est pas actualisée. Mais après avoir actualisé la page manuellement, elle affiche les données nouvellement saisies.
Voici mon composant DevelopmentList .
class AddNewDevelopmentWork extends Component {
state = {
open: false,
location: "",
work_description: "",
date_of_commencement: new Date(),
date_of_completion: new Date(),
status_of_work: "",
vertical: "top",
horizontal: "center"
};
handleCommencementDateChange = date => {
this.setState({
date_of_commencement: date
});
};
handleCompletionDateChange = date => {
this.setState({
date_of_completion: date
});
};
handleToggle = () => {
this.setState({
open: !this.state.open
});
};
handleClickOpen = () => {
this.setState({ open: true });
};
handleClose = () => {
this.props.history.push("/dashboard/developmentworks");
};
onChange = e => {
const state = this.state;
state[e.target.name] = e.target.value;
this.setState(state);
};
handleFormSubmit = e => {
e.preventDefault();
const {
location,
work_description,
date_of_commencement,
date_of_completion,
status_of_work
} = this.state;
axios
.post("/api/developmenties/", {
location,
work_description,
date_of_commencement,
date_of_completion,
status_of_work
})
.then(() => {
// this.props.history.push("/dashboard/developmentworks");
// window.location.href = window.location.href;
this.setState({
open: false,
vertical: "top",
horizontal: "center"
});
})
.catch(error => {
alert("Ooops something wrong happened, please try again");
});
};
handleCancel = () => {
this.setState({ open: false });
};
render() {
const { classes } = this.props;
const {
location,
work_description,
date_of_commencement,
date_of_completion,
status_of_work,
vertical,
horizontal
} = this.state;
return (
<MuiThemeProvider theme={theme}>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<div>
<MuiThemeProvider theme={theme}>
<Dialog open={this.state.open} onClose={this.handleToggle}>
<DialogContent required>
<form onSubmit={this.handleFormSubmit}>
<TextField
value={location}
onChange={e =>
this.setState({
location: e.target.value
})
}
error={location === ""}
helperText={
location === "" ? "Please enter Location" : " "
}
label="Location"
type="email"
fullWidth
/>
<TextField
value={work_description}
onChange={e =>
this.setState({
work_description: e.target.value
})
}
error={work_description === ""}
helperText={
work_description === ""
? "Please enter Work Description"
: " "
}
label="Description of Work"
type="email"
fullWidth
/>
<div>
<DatePicker
format="dd/MM/yyyy"
label="Date of Commencement"
value={date_of_commencement}
onChange={this.handleCommencementDateChange}
disableOpenOnEnter
animateYearScrolling={false}
/>
</div>
<div>
<DatePicker
format="dd/MM/yyyy"
label="Date of Completion"
value={date_of_completion}
onChange={this.handleCompletionDateChange}
/>
</div>
<TextField
value={status_of_work}
onChange={e =>
this.setState({
status_of_work: e.target.value
})
}
error={location === ""}
helperText={
status_of_work === ""
? "Please enter Status of Work!"
: " "
}
label="Status of Work"
type="email"
fullWidth
/>
</form>
</DialogContent>
<DialogActions>
<Button
onClick={this.handleCancel}
classes={{
root: classes.root
}}
variant="contained"
>
Cancel
</Button>
<Button
onClick={this.handleFormSubmit}
color="primary"
variant="contained"
>
Save
</Button>
</DialogActions>
</Dialog>
</MuiThemeProvider>
</div>
</MuiPickersUtilsProvider>
</MuiThemeProvider>
);
}
}
Cependant, avec la méthode componentDidUpdate ci-dessous, si je la change comme ci-dessous la condition if (en retirant la propriété de longueur de l'équation) alors les nouvelles données sont immédiatement rendues sur la page, mais elles deviennent également une boucle infinie à l'intérieur de componentDidUpdate et frappent encore et encore l'API Express chaque seconde.
componentDidUpdate(prevProps, prevState) {
if (
this.state.allDevelopmentWorks !==
prevState.allDevelopmentWorks
) {
return axios
.get("/api/developmenties")
.then(res => {
this.setState({
allDevelopmentWorks: res.data
});
})
.catch(function(error) {
console.log(error);
});
}
}
Le code du deuxième composant, (qui est le composant enfant du composant principal DevelopmentList.js , qui ouvrira uniquement une boîte de dialogue pour que l'utilisateur saisisse et remplisse quelques TestFields ajoute de nouvelles données à ce CRUD) est ci-dessous AddNewDevelopmentWork.js
class DevelopmentList extends Component {
constructor(props) {
super(props);
this.state = {
allDevelopmentWorks: []
};
}
componentDidUpdate(prevProps, prevState) {
if (
this.state.allDevelopmentWorks.length !==
prevState.allDevelopmentWorks.length
) {
return axios
.get("/api/developmenties")
.then(res => {
this.setState({
allDevelopmentWorks: res.data
});
})
.catch(function(error) {
console.log(error);
});
}
}
componentDidMount() {
axios.get("/api/developmenties").then(res => {
this.setState({
allDevelopmentWorks: res.data
});
});
}
render() {
const { classes } = this.props;
return (
<div>
<Table className={classes.table}>
<TableHead>
<TableRow className={classes.row}>
<CustomTableCell align="left">Location</CustomTableCell>
<CustomTableCell align="left">
Description Of Work
</CustomTableCell>
<CustomTableCell align="left">
Date of Commencement
</CustomTableCell>
<CustomTableCell align="left">Date of Completion</CustomTableCell>
<CustomTableCell align="left">Status of Work</CustomTableCell>
</TableRow>
</TableHead>
<TableBody>
{this.state.allDevelopmentWorks.map((document, i) => (
<TableRow className={classes.row} key={i}>
<CustomTableCell component="th" scope="row">
{document.location}
</CustomTableCell>
<CustomTableCell align="left">
{document.work_description}
</CustomTableCell>
<CustomTableCell align="left">
{moment(document.date_of_commencement).format("YYYY-MM-DD")}
</CustomTableCell>
<CustomTableCell align="left">
{moment(document.date_of_completion).format("YYYY-MM-DD")}
</CustomTableCell>
<CustomTableCell align="left">
{document.status_of_work}
</CustomTableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
}
export default withStyles(styles)(DevelopmentList);
4 Réponses :
Consultez cette réponse pour mettre à jour un tableau d'état sans le muter.
Et jetez un œil à loadash isEqual pour comparer l'égalité des tableaux.
La boucle infinie qui en résulte n'a pas grand-chose à voir avec React lui-même mais plutôt avec la façon dont javascript gère la comparaison entre deux objets. Puisque le type de données renvoyé par l'API est un tableau
componentWillMount -> render -> componentDidMount(state changes here will trigger a re-render from maybe an api fetch) -> componentWillUpdate -> render -> componentDidUpdate
La condition sera toujours vraie et donc setState est appelé à plusieurs reprises pour chaque changement d'état et cela déclenchera un nouveau rendu. Une meilleure compréhension de la façon dont les différentes méthodes de cycle de vie des composants sont invoquées est l'un des concepts de base dont j'ai dû me familiariser au début de mon apprentissage de React.
[] !== [] => // true
Vous pouvez partager un lien vers le dépôt s'il y en a un et je peux y jeter un œil
Si vous utilisez setState à l'intérieur de componentDidUpdate, il met à jour le composant, ce qui entraîne un appel à componentDidUpdate qui appelle à nouveau setState résultant en une boucle infinie. Vous devez appeler sous condition setState et vous assurer que la condition violant l'appel se produira éventuellement, par exemple:
componentDidUpdate: function() {
if (condition) {
this.setState({..})
} else {
//do something else
}
}
Si vous ne mettez à jour le composant qu'en lui envoyant des accessoires (il n'est pas mis à jour par setState, sauf pour le cas à l'intérieur de componentDidUpdate), vous pouvez appeler setState à l'intérieur de componentWillReceiveProps au lieu de componentDidUpdate.
Répondre à ma propre question, après avoir résolu le problème. C'était un problème de ne pas mettre à jour l'état du composant parent ( DevelopmentList.js ) lorsque l'utilisateur ajoutait un nouvel élément avec le composant enfant. ( AddNewDevelopmentWork.js qui est une boîte de dialogue de formulaire). Il s'agissait donc de passer des données de l'enfant au parent pour mettre à jour l'état du parent comme ci-dessous
A> Définir un rappel dans mon parent (fonction addItem ) qui prend les données dont j'ai besoin en tant que paramètre.
B> Transmettez ce rappel comme accessoire à l'enfant
C> Appelez le rappel en utilisant this.props. [callback] à l'intérieur de l'enfant,
et transmettez les données comme argument.
Voici mon code de travail final dans le parent DevelopmentList.js
class AddNewDevelopmentWork extends Component {
state = {
open: false,
opensnackbar: false,
vertical: "top",
horizontal: "center",
location: "",
work_description: "",
date_of_commencement: new Date(),
date_of_completion: new Date(),
status_of_work: ""
};
handleCommencementDateChange = date => {
this.setState({
date_of_commencement: date
});
};
handleCompletionDateChange = date => {
this.setState({
date_of_completion: date
});
};
handleToggle = () => {
this.setState({
open: !this.state.open
});
};
handleClickOpen = () => {
this.setState({ open: true });
};
handleClose = () => {
this.setState({ opensnackbar: false });
this.props.history.push("/dashboard/developmentworks");
};
onChange = e => {
const state = this.state;
state[e.target.name] = e.target.value;
this.setState(state);
};
handleFormSubmit = e => {
e.preventDefault();
const { addNewItemToParentState } = this.props;
const {
location,
work_description,
date_of_commencement,
date_of_completion,
status_of_work
} = this.state;
axios
.post("/api/developmenties/", {
location,
work_description,
date_of_commencement,
date_of_completion,
status_of_work
})
.then(() => {
addNewItemToParentState({
location,
work_description,
date_of_commencement,
date_of_completion,
status_of_work
});
this.setState({
open: false,
opensnackbar: true,
vertical: "top",
horizontal: "center"
});
})
.catch(error => {
alert("Ooops something wrong happened, please try again");
});
};
handleCancel = () => {
this.setState({ open: false });
};
render() {
const { classes } = this.props;
const {
location,
work_description,
date_of_commencement,
date_of_completion,
status_of_work,
vertical,
horizontal,
opensnackbar
} = this.state;
return (
<MuiThemeProvider theme={theme}>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<div>
<MuiThemeProvider theme={theme}>
<Fab
variant="fab"
onClick={this.handleClickOpen}
aria-pressed="true"
color="secondary"
size="large"
aria-label="Add"
fontSize="large"
>
<AddIcon className={styles.largeIcon} />
</Fab>
<Dialog
open={this.state.open}
onClose={this.handleToggle}
aria-labelledby="form-dialog-title"
fullWidth={true}
maxWidth={"md"}
>
<DialogTitle
id="form-dialog-title"
disableTypography="false"
className={this.props.classes.styledHeader}
>
New Development Work
</DialogTitle>
<DialogContent required>
<form onSubmit={this.handleFormSubmit}>
<TextField
value={location}
onChange={e =>
this.setState({
location: e.target.value
})
}
type="email"
/>
<TextField
value={work_description}
onChange={e =>
this.setState({
work_description: e.target.value
})
}
type="email"
/>
<div>
<DatePicker
value={date_of_commencement}
onChange={this.handleCommencementDateChange}
/>
</div>
<div>
<DatePicker
value={date_of_completion}
onChange={this.handleCompletionDateChange}
/>
</div>
<TextField
value={status_of_work}
onChange={e =>
this.setState({
status_of_work: e.target.value
})
}
type="email"
fullWidth
/>
</form>
</DialogContent>
<DialogActions>
<Button
onClick={this.handleCancel}
classes={{
root: classes.root
}}
variant="contained"
>
Cancel
</Button>
<Button
onClick={this.handleFormSubmit}
color="primary"
variant="contained"
>
Save
</Button>
</DialogActions>
</Dialog>
<Snackbar
anchorOrigin={{ vertical, horizontal }}
open={opensnackbar}
autoHideDuration={2000}
onClose={this.handleClose}
>
<MySnackbarContent
onClose={this.handleClose}
variant="success"
message="New Development Works has been uploaded successfully"
/>
</Snackbar>
</MuiThemeProvider>
</div>
</MuiPickersUtilsProvider>
</MuiThemeProvider>
);
}
}
AddNewDevelopmentWork.propTypes = {
classes: PropTypes.object.isRequired
};
export default withStyles(styles)(AddNewDevelopmentWork);
Et voici mon code de travail final dans l'enfant AddNewDevelopmentWork.js
class DevelopmentList extends Component {
constructor(props) {
super(props);
this.state = {
allDevelopmentWorks: []
};
}
addItem = item => {
this.setState({
allDevelopmentWorks: [item, ...this.state.allDevelopmentWorks]
});
};
componentDidMount() {
axios.get("/api/developmenties").then(res => {
this.setState({
allDevelopmentWorks: res.data
});
});
}
componentDidUpdate(prevProps, prevState) {
if (
this.state.allDevelopmentWorks.length !==
prevState.allDevelopmentWorks.length
) {
return axios
.get("/api/developmenties")
.then(res => {
this.setState({
allDevelopmentWorks: res.data
});
})
.catch(function(error) {
console.log(error);
});
}
}
deleteDevelopmentWorks = id => {
axios.delete("/api/developmenties/" + id).then(() => {
this.setState({
allDevelopmentWorks: this.state.allDevelopmentWorks.filter(
item => item._id !== id
)
});
});
};
render() {
const { classes } = this.props;
return (
<div>
<AddNewDevelopmentWork addNewItemToParentState={this.addItem} />
<Table className={classes.table}>
<TableBody>
{this.state.allDevelopmentWorks.map((document, i) => (
<TableRow className={classes.row} key={i}>
<CustomTableCell component="th" scope="row">
{document.location}
</CustomTableCell>
<CustomTableCell align="left">
{document.work_description}
</CustomTableCell>
<CustomTableCell align="left">
{moment(document.date_of_commencement).format("YYYY-MM-DD")}
</CustomTableCell>
<CustomTableCell align="left">
{moment(document.date_of_completion).format("YYYY-MM-DD")}
</CustomTableCell>
<CustomTableCell align="left">
{document.status_of_work}
</CustomTableCell>
<CustomTableCell align="left">
<div id="snackbar">
The Document has been successfully deleted
</div>
<Button
onClick={this.deleteDevelopmentWorks.bind(
this,
document._id
)}
variant="contained"
className={classes.button}
>
<DeleteIcon className={classes.rightIcon} />
</Button>
</CustomTableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
}
export default withStyles(styles)(DevelopmentList);
Copie possible de setState () à l'intérieur de componentDidUpdate ()