javascript - React setState memperbarui komponen anak yang salah
Saya sedang membangun sistem posting blog di mana pengguna dapat membuat beberapa posting yang semuanya ditampilkan pada satu halaman dan dapat mengeditnya dengan editor TinyMCE langsung di halaman yang sama jika diperlukan.
Setiap kiriman blog individu adalah Komponen React sendiri, secara khusus disebut Post. Kelas Komponen Post bertanggung jawab untuk merender Post (misalnya judul, isi, penulis, dll.), ini termasuk formulir edit post. Komponen ini mendapatkan data Post seperti judul dan isi dari props yang diteruskan oleh komponen App.
Komponen App adalah titik masuk, ia mengambil semua posting blog dalam format JSON dari server saya, membuat komponen Post untuk masing-masing, meneruskan props yang sesuai dan mendorong seluruh komponen Post ke dalam sebuah array. Setelah ini selesai, ia memanggil this.setState() dan memperbarui array posts.
Namun, jika beberapa posting dibuat dan saya memanggil this.handleEdit() untuk satu komponen Post, itu akan memperbarui state komponen Post yang salah.
App.tsx
class App extends React.Component<IProps, IState> {
constructor(props: Readonly<IProps>) {
super(props);
this.state = {
posts: []
}
}
componentWillMount = () => {
var req = new HTTPRequest("GET", "/qa/posts")
req.execVoid(HTTP.RESPONSE.OK).then(function (data: []) {
var posts = [];
data.forEach(function (entry, index) {
posts.push(
<Post
id={entry['id']}
title={entry['title']}
body={entry['body']}
author={entry['author']}
date={entry['created_at']}
showDate={entry['showDate']}
deletePost={() => this.deleteComponent(index)}
key={index}
/>
)
}.bind(this))
this.updatePosts(posts)
}.bind(this))
}
updatePosts = (posts: Array<any>) => {
if (posts.length == 0) {
posts.push(
<div className="card" key={1}>
<div className="card-content">
No posts to show :)
</div>
</div>
)
}
this.setState({
posts: posts
})
}
deleteComponent = (key: number) => {
let posts = this.state.posts.filter(function (value, index) {
return index != key;
})
this.updatePosts(posts);
}
componentDidMount = () => {
}
render(): React.ReactNode {
return (
<div>
{this.state.posts}
</div>
)
}
}
export default App;
Ketika saya mengklik tombol "Cancel", yang ditampilkan di metode this.actions(), dengan this.state.editEnabled disetel ke true, itu tidak akan memperbarui status dari kelas Post saat ini, melainkan tampaknya memperbarui Post lain di array posts yang dibuat App. Secara khusus, tombol "Cancel" akan memanggil this.disableEdit() yang memperbarui this.state.editEnabled menjadi false. Namun itu tidak dilakukan untuk Post saat ini, melainkan Post lain di array, seolah-olah secara acak... mencoba mencetak judul post yang terkait dengan post tersebut juga akan memberikan judul post yang salah seperti yang dapat Anda lihat di this.disableEdit()
Post.tsx
class Post extends React.Component<IProps, IState> {
constructor(props: Readonly<IProps>) {
super(props);
this.state = {
id: -1,
title: "",
body: "",
author: "",
date: "",
showDate: true,
editEnabled: false,
showProgressBar: false,
edit: {
title: "",
}
};
}
componentDidMount = () => {
this.setState({
id: this.props['id'],
title: this.props['title'],
body: this.props['body'],
author: "",//this.props['author'],
date: this.convertToReadableDate(this.props['date']),
showDate: !!this.props['showDate'],
})
tinymce.init({
selector: "#edit_body_" + this.props['id'],
skin_url: '/lib/tinymce/skins/ui/oxide',
})
}
convertToReadableDate(unix_timestamp: number): string {
var date = new Date(unix_timestamp * 1000);
return date.toISOString().split("T")[0];
}
handleDelete = () => {
if (confirm("Are you sure you would like to delete this post?")) {
var req = new HTTPRequest("DELETE", "/qa/posts/" + this.state.id);
req.execVoid(HTTP.RESPONSE.OK)
.then(function () {
this.props.deletePost();
M.toast({ html: "Your post was deleted!", classes: "green" })
}.bind(this))
.catch(function (err: Error) {
M.toast({
html: "We have trouble deleting your post. Try again later",
classes: "red"
});
console.error(err.message);
}.bind(this))
}
}
promptSaveChange = () => {
if (this.state.title != this.state.edit.title || tinymce.get('edit_body_' + this.props.id).getContent() !== this.state.body) {
return confirm("You have unsaved changes. Are you sure you would like to proceed?")
} else {
return true;
}
}
handleEdit = () => {
if (this.state.editEnabled) {
if (this.promptSaveChange()) {
this.disableEdit();
}
} else {
this.enableEdit();
tinymce.get('edit_body_' + this.props.id).setContent(this.state.body);
}
}
resetChanges = () => {
this.setState({
edit: {
title: this.state.title
}
})
tinymce.get('edit_body_' + this.props.id).setContent(this.state.body);
}
handleEditSave = () => {
this.showProgress();
var req = new HTTPRequest("PATCH", "/qa/posts/" + this.state.id);
var body_content = tinymce.get('edit_body_' + this.props.id).getContent();
req.execAsJSON({
title: this.state.edit.title,
body: body_content
}, HTTP.RESPONSE.ACCEPTED).then(function (ret) {
this.setState({
title: this.state.edit.title,
body: body_content
});
this.disableEdit();
M.toast({
html: ret['msg'],
classes: 'green'
})
}.bind(this)).catch(function (err: Error) {
console.log(err.message);
M.toast({
html: "We had trouble updating the post. Try again later."
})
}.bind(this)).finally(function () {
this.hideProgress();
})
}
handleTitleEdit = (e) => {
this.setState({
edit: {
title: e.target.value
}
})
}
enableEdit = () => {
this.setState({
editEnabled: true,
edit: {
title: this.state.title
}
}, function () {
M.AutoInit();
})
}
disableEdit = () => {
console.log('disabled: ' + this.state.title);
this.setState({
editEnabled: false
})
}
showProgress = () => {
this.setState({
showProgressBar: true
})
}
hideProgress = () => {
this.setState({
showProgressBar: false
})
}
content = () => {
return (
<div>
<div style={{ display: this.state.editEnabled ? 'none' : null }}>
<span className="card-title">{this.state.title}</span>
<div dangerouslySetInnerHTML={{ __html: this.state.body }}></div>
<small> {this.state.showDate ? "Posted at: " + this.state.date : ""}
</div>
<div style={{ display: this.state.editEnabled ? null : 'none' }}>
<input type="text" name="title" value={this.state.edit.title} placeholder={this.state.title} onChange={this.handleTitleEdit} />
<textarea id={"edit_body_" + this.props.id}></textarea>
</div>
</div>
)
}
actions = () => {
return (
<>
<div className="row" style={{ display: this.state.editEnabled ? null : 'none' }}>
<a className="btn-small green waves-effect" onClick={this.handleEditSave}><i className="material-icons left">save</i> Save</a>
<a className='dropdown-trigger btn-flat blue-text' href='#' data-target='edit-options'>More</a>
<ul id='edit-options' className='dropdown-content'>
<li>
<a href="#!" className="orange-text" onClick={this.resetChanges}>Reset Changes</a>
</li>
<li>
<a href="#!" className="orange-text" onClick={this.handleEdit}>Cancel</a>
</li>
<li>
<a href="#!" className="red-text" onClick={this.handleDelete}>Delete</a>
</li>
</ul>
<div className="progress" style={{ display: this.state.showProgressBar ? null : "none" }}>
<div className="indeterminate"></div>
</div>
</div>
<div className="row" style={{ display: this.state.editEnabled ? 'none' : null }}>
<a className="btn-small orange waves-effect" onClick={this.handleEdit}><i className="material-icons left">edit</i> Edit</a>
</div>
</>
)
}
render(): React.ReactNode {
return (
<div className="card">
<div className="card-content">
{this.content()}
</div>
<div className="card-action">
{this.actions()}
</div>
</div>
)
}
}
export default Post;
Jawaban #1
OK. Saya telah menyelesaikan masalah menarik ini. Ternyata React bukan masalahnya. Framework CSS Materialize yang saya gunakan yang membuat masalah, khususnya M.AutoInit()
Memanggilnya di tempat yang salah dapat menyebabkan masalah dengan penangan acara dari React.