I'm building a blog post system where users can create multiple posts all displayed on a single page and can edit them with a TinyMCE editor directly on the same page if needed.
Each individual blog post is it's own React Component, specifically called Post. The Post Component class is responsible for rendering the Post (ie title, body, author etc.), this includes the edit post form. It gets the Post data such as title and body from props which was passed down by the App component.
The App component is the entry point, it retrieves all blog posts in JSON format from my server, creates a Post component for each one, passes down the corresponding props and pushes the entire Post component into an array. Once this is complete, it calls this.setState() and updates the posts array.
However, if multiple posts are created and I call this.handleEdit() for and individual Post component, it will update the wrong Post component's state.
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;
When I click the "Cancel" button, shown in the this.actions() method, with this.state.editEnabled set to true, it won't update the state of the current Post class, instead it seems to update another Post in the posts array App created. In particular, the "Cancel" button will call this.disableEdit() which updates the this.state.editEnabled to false. However it doesn't do it for the current Post, but another Post in the array, seemingly at random... trying to print out the post title associated with the post will also give the incorrect post title as you can see in 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 : ""}</small>
</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;
componentDidMountdoesn't seem to resolve this issue though. I tried refreshing cache and what not - David Yue