2
votes

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;   
1
@JuniusL. damn.. does that mean I will have to use componentDidMount? I originally used it, but found it ridiculously slow when trying to render posts on the page. Hence I switched to componentWillMount. Didn't know it was deprecated.. - David Yue
Hmmm. changing it to componentDidMount doesn't seem to resolve this issue though. I tried refreshing cache and what not - David Yue
yeah, it won't, maybe some posts have the same id? - Junius L.
They could not have the same id, the id comes from a mysql db primary key with auto increment. I've also check in the chrome react debugger, they were different. - David Yue

1 Answers

0
votes

OK. I have solved this interesting issue. Turns out React is not the problem. The Materialize CSS Framework I was using created the problem, specifically M.AutoInit()

Calling it at the wrong place can cause issues with event handlers from React.