ค้นหาเว็บไซต์

javascript - React setState อัปเดตคอมโพเนนต์ลูกไม่ถูกต้อง


ฉันกำลังสร้างระบบบล็อกโพสต์ที่ผู้ใช้สามารถสร้างโพสต์ได้หลายโพสต์ซึ่งจะแสดงทั้งหมดบนหน้าหนึ่งเดียวและสามารถแก้ไขโพสต์เหล่านั้นด้วยตัวแก้ไข TinyMCE ได้โดยตรงบนหน้าดังกล่าวถ้าจำเป็น

แต่ละโพสต์ในบล็อกเป็นคอมโพเนนต์ React ของตัวเอง โดยเฉพาะเรียกว่า Post คลาสคอมโพเนนต์ Post รับผิดชอบในการแสดงผล Post (เช่น ชื่อเรื่อง เนื้อหา ผู้เขียน ฯลฯ) ซึ่งรวมถึงฟอร์มแก้ไขโพสต์ด้วย มันได้รับข้อมูล Post เช่น ชื่อเรื่องและเนื้อหาจาก props ซึ่งถูกส่งลงมาจากคอมโพเนนต์ App

คอมโพเนนต์ App เป็นจุดเริ่มต้น มันดึงโพสต์บล็อกทั้งหมดในรูปแบบ JSON จากเซิร์ฟเวอร์ของฉัน สร้างคอมโพเนนต์ Post สำหรับแต่ละโพสต์ ส่ง props ที่ตรงกันลงไป และผลักคอมโพเนนต์ Post ทั้งหมดเข้าไปในอาร์เรย์ เมื่อขั้นตอนนี้เสร็จสมบูรณ์ มันเรียก this.setState() และอัปเดตอาร์เรย์ posts

อย่างไรก็ตาม ถ้ามีการสร้างโพสต์หลายรายการและฉันเรียก this.handleEdit() สำหรับคอมโพเนนต์โพสต์แต่ละรายการ มันจะอัปเดตสถานะของคอมโพเนนต์โพสต์ที่ผิด

แอป.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;

เมื่อฉันคลิกปุ่ม "ยกเลิก" ซึ่งแสดงในเมธอด this.actions() โดยมี this.state.editEnabled ตั้งค่าเป็น true มันจะไม่อัปเดตสถานะของคลาส Post ปัจจุบัน แต่ดูเหมือนว่าจะอัปเดต Post อื่นในอาร์เรย์ posts ที่แอปสร้างขึ้น โดยเฉพาะ ปุ่ม "ยกเลิก" จะเรียก this.disableEdit() ซึ่งจะอัปเดต this.state.editEnabled เป็น false อย่างไรก็ตาม มันไม่ได้ทำกับ Post ปัจจุบัน แต่กับ Post อื่นในอาร์เรย์ ดูเหมือนจะสุ่ม... การพยายามพิมพ์ชื่อโพสต์ที่เกี่ยวข้องกับโพสต์นั้นก็จะให้ชื่อโพสต์ที่ไม่ถูกต้องเช่นเดียวกันตามที่คุณเห็นใน this.disableEdit()

โพสต์.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;   

คำตอบ #1

โอเค ฉันได้แก้ไขปัญหาที่น่าสนใจนี้แล้ว ปรากฏว่า React ไม่ใช่สาเหตุ ปัญหามาจาก Materialize CSS Framework ที่ฉันกำลังใช้ โดยเฉพาะ M.AutoInit()

การเรียกใช้มันในตำแหน่งที่ผิดสามารถทำให้เกิดปัญหากับตัวจัดการเหตุการณ์จาก React ได้