this.setState ไม่ได้รวมสถานะเหมือนที่ฉันคาดไว้


160

ฉันมีสถานะดังต่อไปนี้:

this.setState({ selected: { id: 1, name: 'Foobar' } });  

จากนั้นฉันจะอัปเดตสถานะ:

this.setState({ selected: { name: 'Barfoo' }});

เนื่องจากsetStateคาดว่าจะรวมฉันจะคาดหวังว่ามันจะเป็น:

{ selected: { id: 1, name: 'Barfoo' } }; 

แต่มันกิน id และสถานะคือ:

{ selected: { name: 'Barfoo' } }; 

นี่เป็นพฤติกรรมที่คาดหวังหรือไม่และนี่คือวิธีแก้ปัญหาในการอัปเดตคุณสมบัติเดียวของวัตถุสถานะซ้อนกันหรือไม่

คำตอบ:


143

ฉันคิดว่าsetState()ไม่รวมเวียนเกิดซ้ำ

คุณสามารถใช้ค่าของสถานะปัจจุบันthis.state.selectedเพื่อสร้างสถานะใหม่แล้วเรียกสิ่งนั้นได้setState()ว่า:

var newSelected = _.extend({}, this.state.selected);
newSelected.name = 'Barfoo';
this.setState({ selected: newSelected });

ฉันได้ใช้ฟังก์ชั่นฟังก์ชั่_.extend()น (จากห้องสมุด underscore.js) ที่นี่เพื่อป้องกันการเปลี่ยนแปลงในselectedส่วนที่มีอยู่ของรัฐโดยการสร้างสำเนาตื้น ๆ ของมัน

วิธีแก้ปัญหาอื่นก็คือการเขียนsetStateRecursively()ซึ่งจะรวมการเรียกซ้ำในสถานะใหม่แล้วเรียกใช้replaceState()ด้วย:

setStateRecursively: function(stateUpdate, callback) {
  var newState = mergeStateRecursively(this.state, stateUpdate);
  this.replaceState(newState, callback);
}

7
ทำงานนี้ได้อย่างน่าเชื่อถือหากคุณทำมากกว่าหนึ่งครั้งหรือมีโอกาสตอบสนองจะเข้าคิวแทนที่การโทรสถานะและสุดท้ายจะชนะหรือไม่
edoloughlin

111
สำหรับคนที่ชอบใช้การขยายและที่ใช้ babel คุณสามารถทำได้this.setState({ selected: { ...this.state.selected, name: 'barfoo' } })ซึ่งได้รับการแปลเป็นthis.setState({ selected: _extends({}, this.state.selected, { name: 'barfoo' }) });
Pickels

8
@Pickels นี้เป็นสิ่งที่ดีอย่างสำนวนสำหรับการตอบสนองและไม่ต้องใช้/underscore lodashได้โปรดหมุนมันออกไปเป็นคำตอบของมันเอง
ericsoco

14
this.state ไม่จำเป็นต้องขึ้นไปวันที่ คุณสามารถได้รับผลลัพธ์ที่ไม่คาดคิดโดยใช้วิธีการขยาย การส่งผ่านฟังก์ชั่นการอัพเดทไปsetStateเป็นทางออกที่ถูกต้อง
สตอร์ม

1
@ EdwardD'Souza มันเป็นห้องสมุดบ้านพัก มันถูกเรียกโดยทั่วไปว่าเป็นขีดล่างเช่นเดียวกับ jQuery ที่ถูกเรียกโดยทั่วไปว่าเป็นดอลลาร์ (ดังนั้นมันคือ _.extend ())
mjk

96

ผู้ช่วยเหลือที่ไม่สามารถเปลี่ยนแปลงได้ถูกเพิ่มเข้ามาใน React.addons เมื่อเร็ว ๆ นี้คุณสามารถทำสิ่งต่อไปนี้ได้:

var newState = React.addons.update(this.state, {
  selected: {
    name: { $set: 'Barfoo' }
  }
});
this.setState(newState);

เอกสารผู้ช่วยที่ไม่สามารถเปลี่ยนแปลงได้


7
การอัปเดตเลิกใช้แล้ว facebook.github.io/react/docs/update.html
thedanotto

3
@thedanotto ดูเหมือนว่าReact.addons.update()วิธีการจะเลิก แต่ห้องสมุดทดแทนgithub.com/kolodny/immutability-helperมีupdate()ฟังก์ชั่นที่เทียบเท่า
Bungle

37

เนื่องจากคำตอบหลายข้อใช้สถานะปัจจุบันเป็นพื้นฐานสำหรับการผสานข้อมูลใหม่ฉันต้องการชี้ให้เห็นว่าสิ่งนี้อาจผิดพลาดได้ การเปลี่ยนแปลงสถานะถูกจัดคิวและอย่าแก้ไขวัตถุสถานะขององค์ประกอบทันที ข้อมูลสถานะการอ้างอิงก่อนที่จะประมวลผลคิวจึงจะให้ข้อมูลเก่าที่ไม่สะท้อนการเปลี่ยนแปลงที่รอดำเนินการใน setState จากเอกสาร:

setState () ไม่ได้ทำการเปลี่ยนแปลงในทันทีนี้ แต่สร้างการเปลี่ยนแปลงสถานะที่รอดำเนินการ การเข้าถึง this.state หลังจากเรียกใช้วิธีนี้อาจส่งคืนค่าที่มีอยู่

ซึ่งหมายความว่าการใช้สถานะ "ปัจจุบัน" เป็นการอ้างอิงในการเรียก setState ที่ตามมานั้นไม่น่าเชื่อถือ ตัวอย่างเช่น:

  1. การเรียกครั้งแรกเพื่อ setState การจัดคิวการเปลี่ยนแปลงสถานะวัตถุ
  2. การเรียกครั้งที่สองเพื่อ setState สถานะของคุณใช้วัตถุที่ซ้อนกันดังนั้นคุณจึงต้องการผสาน ก่อนที่จะเรียก setState คุณจะได้รับวัตถุสถานะปัจจุบัน วัตถุนี้ไม่ได้สะท้อนการเปลี่ยนแปลงที่อยู่ในคิวในการโทรครั้งแรกเพื่อ setState ด้านบนเนื่องจากยังคงเป็นสถานะดั้งเดิมซึ่งตอนนี้ควรได้รับการพิจารณาว่าเป็น "เก่า"
  3. ดำเนินการผสาน ผลลัพธ์คือสถานะ "เก่า" ดั้งเดิมรวมถึงข้อมูลใหม่ที่คุณเพิ่งตั้งค่าการเปลี่ยนแปลงจากการเริ่มต้น setState การโทรจะไม่สะท้อนให้เห็น setState ของคุณโทรเข้าคิวการเปลี่ยนแปลงที่สองนี้
  4. ตอบสนองต่อกระบวนการคิว มีการประมวลผลการโทร setState ครั้งแรกอัปเดตสถานะ มีการประมวลผลการเรียก setState ที่สองอัพเดตสถานะ อ็อบเจกต์ setState ตัวที่สองแทนที่ตอนแรกแล้วและเนื่องจากข้อมูลที่คุณมีเมื่อทำการโทรนั้นเป็นข้อมูลเก่าข้อมูลเก่าที่ถูกแก้ไขจากการโทรครั้งที่สองนี้ได้ขัดขวางการเปลี่ยนแปลงที่เกิดขึ้นในการโทรครั้งแรกซึ่งหายไป
  5. เมื่อคิวว่าง React จะกำหนดว่าจะให้แสดง ฯลฯ ณ จุดนี้คุณจะทำการเปลี่ยนแปลงที่เกิดขึ้นในการเรียก setState ครั้งที่สองและมันจะเหมือนกับว่าการเรียก setState ครั้งแรกไม่เคยเกิดขึ้น

หากคุณต้องการใช้สถานะปัจจุบัน (เช่นเพื่อรวมข้อมูลลงในวัตถุที่ซ้อนกัน) setState หรือยอมรับฟังก์ชั่นเป็นอาร์กิวเมนต์แทนวัตถุ ฟังก์ชั่นนี้ถูกเรียกใช้หลังจากการอัพเดตก่อนหน้านี้ไปยังสถานะและผ่านสถานะเป็นอาร์กิวเมนต์ - ดังนั้นสิ่งนี้สามารถนำมาใช้เพื่อทำการเปลี่ยนแปลงอะตอมรับประกันว่าจะเคารพการเปลี่ยนแปลงก่อนหน้านี้


5
มีตัวอย่างหรือเอกสารมากกว่าหนึ่งอย่างเกี่ยวกับวิธีการทำเช่นนี้? afaik ไม่มีใครคำนึงถึงสิ่งนี้
Josef.B

@Dennis ลองคำตอบของฉันด้านล่าง
Martin Dawson

ฉันไม่เห็นว่าปัญหาอยู่ที่ไหน สิ่งที่คุณกำลังอธิบายจะเกิดขึ้นก็ต่อเมื่อคุณพยายามเข้าถึงสถานะ "ใหม่" หลังจากเรียกใช้ setState นั่นเป็นปัญหาที่แตกต่างอย่างสิ้นเชิงที่ไม่มีส่วนเกี่ยวข้องกับคำถาม OP การเข้าถึงสถานะเก่าก่อนที่จะเรียก setState เพื่อให้คุณสามารถสร้างออบเจ็กต์สถานะใหม่จะไม่มีปัญหาและในความเป็นจริงแล้วเป็นสิ่งที่ React คาดหวัง
nextgentech

@ nextgentech "การเข้าถึงสถานะเก่าก่อนเรียก setState เพื่อให้คุณสามารถสร้างวัตถุสถานะใหม่จะไม่เป็นปัญหา" - คุณเข้าใจผิด เรากำลังพูดถึงสถานะการเขียนทับthis.stateซึ่งอาจค้างอยู่ (หรือค่อนข้างจะอัปเดตรออยู่) เมื่อคุณเข้าถึง
Madbreaks

1
@Madbreaks ตกลงใช่เมื่ออ่านเอกสารอย่างเป็นทางการอีกครั้งฉันเห็นว่ามีการระบุว่าคุณต้องใช้รูปแบบฟังก์ชั่นของ setState เพื่ออัพเดทสถานะได้อย่างน่าเชื่อถือตามสถานะก่อนหน้า ที่กล่าวว่าฉันไม่เคยเจอปัญหาวันที่ถ้าไม่พยายามโทร setState หลายครั้งในฟังก์ชั่นเดียวกัน ขอบคุณสำหรับการตอบกลับที่ทำให้ฉันอ่านรายละเอียดอีกครั้ง
nextgentech

10

ฉันไม่ต้องการติดตั้งไลบรารีอื่นดังนั้นนี่คือวิธีแก้ไขปัญหาอื่น

แทน:

this.setState({ selected: { name: 'Barfoo' }});

ทำสิ่งนี้แทน:

var newSelected = Object.assign({}, this.state.selected);
newSelected.name = 'Barfoo';
this.setState({ selected: newSelected });

หรือขอบคุณ @cc97 ในความคิดเห็นยิ่งกระชับ แต่อ่านง่ายกว่า:

this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });

และเพื่อให้ชัดเจนคำตอบนี้ไม่ได้ละเมิดข้อกังวลใด ๆ ที่ @bgannonpl กล่าวถึงข้างต้น


โหวตขึ้นตรวจสอบ แค่สงสัยว่าทำไมไม่ทำอย่างนี้? this.setState({ selected: Object.assign(this.state.selected, { name: "Barfoo" }));
Justus Romijn

1
@JustusRomijn น่าเสียดายที่คุณจะแก้ไขสถานะปัจจุบันโดยตรง Object.assign(a, b)ใช้เวลาแอตทริบิวต์จากbแทรก / เขียนทับพวกเขาในและจากนั้นผลตอบแทนa aตอบสนองคนที่จะได้รับ uppity ถ้าคุณแก้ไขสถานะโดยตรง
Ryan Shillington

เผง เพิ่งทราบว่ามันใช้งานได้เฉพาะสำหรับการคัดลอก / มอบหมายตื้น
Madbreaks

1
@JustusRomijn คุณสามารถทำเช่นนี้ต่อMDN กำหนดในหนึ่งบรรทัดถ้าคุณเพิ่มเป็นอาร์กิวเมนต์แรก:{} this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });สิ่งนี้ไม่เปลี่ยนแปลงสถานะดั้งเดิม
icc97

@ icc97 Nice! ฉันเพิ่มเข้าไปในคำตอบของฉัน
Ryan Shillington

8

การรักษาสถานะก่อนหน้าโดยยึดตามคำตอบ @bgannonpl:

ตัวอย่างของLodash :

this.setState((previousState) => _.merge({}, previousState, { selected: { name: "Barfood"} }));

เพื่อตรวจสอบว่ามันทำงานอย่างถูกต้องคุณสามารถใช้การเรียกฟังก์ชันพารามิเตอร์ที่สอง:

this.setState((previousState) => _.merge({}, previousState, { selected: { name: "Barfood"} }), () => alert(this.state.selected));

ฉันใช้mergeเพราะextendละทิ้งคุณสมบัติอื่นเป็นอย่างอื่น

ตอบโต้ Immutabilityตัวอย่าง:

import update from "react-addons-update";

this.setState((previousState) => update(previousState, {
    selected:
    { 
        name: {$set: "Barfood"}
    }
});

2

ทางออกของฉันสำหรับสถานการณ์แบบนี้คือการใช้เช่นเดียวกับคำตอบอื่นที่ชี้ให้เห็นผู้ช่วยที่ไม่สามารถเข้าถึงได้

เนื่องจากการตั้งค่าสถานะเชิงลึกเป็นสถานการณ์ทั่วไปฉันจึงสร้างมิกซ์อิน folowing:

var SeStateInDepthMixin = {
   setStateInDepth: function(updatePath) {
       this.setState(React.addons.update(this.state, updatePath););
   }
};

มิกซ์อินนี้รวมอยู่ในส่วนประกอบส่วนใหญ่ของฉันและโดยทั่วไปฉันจะไม่ใช้setStateอีกต่อไปโดยตรง

ด้วย mixin นี้สิ่งที่คุณต้องทำเพื่อให้ได้เอฟเฟกต์ที่ต้องการคือการเรียกใช้ฟังก์ชันsetStateinDepthในวิธีต่อไปนี้:

setStateInDepth({ selected: { name: { $set: 'Barfoo' }}})

สำหรับข้อมูลเพิ่มเติม:

  • เกี่ยวกับการทำงานของ mixins ใน React ดูเอกสารทางการ
  • เกี่ยวกับไวยากรณ์ของพารามิเตอร์ที่ส่งผ่านไปsetStateinDepthดู Immutability ช่วยเอกสาร

ขออภัยที่ตอบช้า แต่ตัวอย่าง Mixin ของคุณน่าสนใจ อย่างไรก็ตามหากฉันมีสถานะซ้อนกัน 2 สถานะเช่น this.state.test.one และ this.state.test.two ถูกต้องหรือไม่ว่ามีเพียงรายการเดียวเท่านั้นที่จะได้รับการอัปเดตและอีกรายการจะถูกลบหรือไม่ เมื่อฉันอัปเดตอันแรกถ้าอันที่สองที่ฉันอยู่ในสถานะนั้นจะถูกลบออก ...
mamruoc

@mamruoc HY, this.state.test.twoจะยังคงมีหลังจากที่คุณปรับปรุงใช้this.state.test.one setStateInDepth({test: {one: {$set: 'new-value'}}})ฉันใช้รหัสนี้ในส่วนประกอบทั้งหมดของฉันเพื่อรับsetStateฟังก์ชันที่จำกัด ของ React
Dorian

1
ฉันพบวิธีแก้ปัญหา ฉันเริ่มต้นthis.state.testเป็นอาร์เรย์ เปลี่ยนเป็นวัตถุแก้มัน
mamruoc

2

ฉันใช้คลาส es6 และฉันลงเอยด้วยวัตถุที่ซับซ้อนหลายอย่างบนสถานะสูงสุดของฉันและพยายามทำให้ส่วนประกอบหลักของฉันเป็นแบบแยกส่วนมากขึ้นดังนั้นฉันจึงสร้างตัวหุ้มคลาสที่เรียบง่ายเพื่อรักษาสถานะขององค์ประกอบด้านบน แต่อนุญาตตรรกะท้องถิ่นเพิ่มเติม .

คลาส wrapper ใช้ฟังก์ชันเป็นตัวสร้างที่ตั้งค่าคุณสมบัติในสถานะองค์ประกอบหลัก

export default class StateWrapper {

    constructor(setState, initialProps = []) {
        this.setState = props => {
            this.state = {...this.state, ...props}
            setState(this.state)
        }
        this.props = initialProps
    }

    render() {
        return(<div>render() not defined</div>)
    }

    component = props => {
        this.props = {...this.props, ...props}
        return this.render()
    }
}

จากนั้นสำหรับแต่ละคุณสมบัติที่ซับซ้อนในสถานะด้านบนฉันสร้างหนึ่งคลาส StateWrapped คุณสามารถตั้งค่าอุปกรณ์ประกอบฉากเริ่มต้นในตัวสร้างที่นี่และพวกเขาจะถูกตั้งค่าเมื่อชั้นเรียนเริ่มต้นคุณสามารถอ้างถึงรัฐในท้องถิ่นสำหรับค่าและการตั้งค่าสถานะท้องถิ่นอ้างถึงฟังก์ชั่นท้องถิ่นและผ่านมันโซ่:

class WrappedFoo extends StateWrapper {

    constructor(...props) { 
        super(...props)
        this.state = {foo: "bar"}
    }

    render = () => <div onClick={this.props.onClick||this.onClick}>{this.state.foo}</div>

    onClick = () => this.setState({foo: "baz"})


}

ดังนั้นองค์ประกอบระดับบนสุดของฉันต้องการเพียงตัวสร้างเพื่อตั้งค่าแต่ละคลาสให้เป็นคุณสมบัติสถานะระดับบนสุดการเรนเดอร์แบบง่ายและฟังก์ชั่นใด ๆ ที่สื่อสารข้ามองค์ประกอบ

class TopComponent extends React.Component {

    constructor(...props) {
        super(...props)

        this.foo = new WrappedFoo(
            props => this.setState({
                fooProps: props
            }) 
        )

        this.foo2 = new WrappedFoo(
            props => this.setState({
                foo2Props: props
            }) 
        )

        this.state = {
            fooProps: this.foo.state,
            foo2Props: this.foo.state,
        }

    }

    render() {
        return(
            <div>
                <this.foo.component onClick={this.onClickFoo} />
                <this.foo2.component />
            </div>
        )
    }

    onClickFoo = () => this.foo2.setState({foo: "foo changed foo2!"})
}

ดูเหมือนว่าจะทำงานได้ค่อนข้างดีสำหรับจุดประสงค์ของฉันโปรดจำไว้ว่าคุณไม่สามารถเปลี่ยนสถานะของคุณสมบัติที่คุณกำหนดให้กับคอมโพเนนต์ที่ถูกห่อที่ส่วนประกอบระดับบนสุดเนื่องจากส่วนประกอบที่ถูกห่อแต่ละรายการกำลังติดตามสถานะของตัวเอง ทุกครั้งที่มีการเปลี่ยนแปลง


2

ณ ตอนนี้

หากสถานะถัดไปขึ้นอยู่กับสถานะก่อนหน้าเราขอแนะนำให้ใช้แบบฟอร์มฟังก์ชั่น updater แทน:

ตามเอกสารhttps://reactjs.org/docs/react-component.html#setstateโดยใช้:

this.setState((prevState) => {
    return {quantity: prevState.quantity + 1};
});

ขอบคุณสำหรับการอ้างอิง / ลิงค์ซึ่งหายไปจากคำตอบอื่น ๆ
Madbreaks

1

สารละลาย

แก้ไข: การแก้ปัญหานี้เคยใช้ไวยากรณ์การแพร่กระจาย เป้าหมายนั้นสร้างวัตถุโดยไม่มีการอ้างอิงใด ๆprevStateเพื่อที่prevStateจะไม่ได้รับการแก้ไข แต่ในการใช้งานของฉันprevStateดูเหมือนจะมีการปรับเปลี่ยนบางครั้ง ดังนั้นสำหรับการโคลนที่สมบูรณ์แบบโดยไม่มีผลข้างเคียงตอนนี้เราเปลี่ยนprevStateเป็น JSON แล้วกลับมาอีกครั้ง (แรงบันดาลใจในการใช้ JSON มาจากMDN )

โปรดจำไว้ว่า:

ขั้นตอน

  1. ทำสำเนาของที่ระดับรากคุณสมบัติของstateที่คุณต้องการเปลี่ยนแปลง
  2. กลายพันธุ์วัตถุใหม่นี้
  3. สร้างวัตถุอัพเดท
  4. ส่งคืนการอัพเดต

ขั้นตอนที่ 3 และ 4 สามารถรวมกันในหนึ่งบรรทัด

ตัวอย่าง

this.setState(prevState => {
    var newSelected = JSON.parse(JSON.stringify(prevState.selected)) //1
    newSelected.name = 'Barfoo'; //2
    var update = { selected: newSelected }; //3
    return update; //4
});

ตัวอย่างที่ง่าย:

this.setState(prevState => {
    var selected = JSON.parse(JSON.stringify(prevState.selected)) //1
    selected.name = 'Barfoo'; //2
    return { selected }; //3, 4
});

สิ่งนี้เป็นไปตามแนวทางของ React อย่างแน่นอน ตามคำตอบของ eickslสำหรับคำถามที่คล้ายกัน


1

โซลูชัน ES6

เรากำหนดสถานะเริ่มแรก

this.setState({ selected: { id: 1, name: 'Foobar' } }); 
//this.state: { selected: { id: 1, name: 'Foobar' } }

เรากำลังเปลี่ยนคุณสมบัติในระดับของวัตถุสถานะ:

const { selected: _selected } = this.state
const  selected = { ..._selected, name: 'Barfoo' }
this.setState({selected})
//this.state: { selected: { id: 1, name: 'Barfoo' } }

0

สถานะการตอบสนองไม่ทำการเวียนเกิดซ้ำในsetStateขณะที่คาดว่าจะไม่มีการอัปเดตสถานะสมาชิกในสถานที่ในเวลาเดียวกัน คุณต้องคัดลอกวัตถุ / อาร์เรย์ที่แนบมาด้วยตัวเอง (ด้วย array.slice หรือ Object.assign) หรือใช้ไลบรารีเฉพาะ

แบบนี้ NestedLinkสนับสนุนการจัดการสถานะการทำปฏิกิริยาแบบผสมโดยตรง

this.linkAt( 'selected' ).at( 'name' ).set( 'Barfoo' );

นอกจากนี้ยังมีการเชื่อมโยงไปยังselectedหรือสามารถส่งผ่านไปทุกที่เป็นเสาเดียวและมีการปรับเปลี่ยนที่นั่นด้วยselected.nameset


-2

คุณตั้งค่าสถานะเริ่มต้นแล้วหรือยัง

ฉันจะใช้รหัสของตัวเองเช่น:

    getInitialState: function () {
        return {
            dragPosition: {
                top  : 0,
                left : 0
            },
            editValue : "",
            dragging  : false,
            editing   : false
        };
    }

ในแอพที่ฉันใช้งานนี่คือวิธีที่ฉันตั้งค่าและใช้งานสถานะ ฉันเชื่อว่าsetStateคุณสามารถแก้ไขสิ่งที่คุณต้องการได้ในแต่ละครั้งที่ฉันเรียกมันว่า:

    onChange: function (event) {
        event.preventDefault();
        event.stopPropagation();
        this.setState({editValue: event.target.value});
    },

โปรดทราบว่าคุณต้องตั้งค่าสถานะภายในReact.createClassฟังก์ชันที่คุณเรียกใช้getInitialState


1
คุณใช้ mixin อะไรในอุปกรณ์ของคุณ สิ่งนี้ไม่ได้ผลสำหรับฉัน
Sachin

-3

ฉันใช้ tmp var เพื่อเปลี่ยน

changeTheme(v) {
    let tmp = this.state.tableData
    tmp.theme = v
    this.setState({
        tableData : tmp
    })
}

2
นี่คือ tmp.theme = v กำลังกลายสถานะ ดังนั้นนี่ไม่ใช่วิธีที่แนะนำ
almoraleslopez

1
let original = {a:1}; let tmp = original; tmp.a = 5; // original is now === Object {a: 5} อย่าทำอย่างนี้แม้ว่ามันจะไม่ได้ทำให้คุณมีปัญหา
Chris
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.