this.setState tidak menggabungkan status seperti yang saya harapkan

160

Saya memiliki status berikut:

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

Lalu saya memperbarui status:

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

Karena setStateseharusnya bergabung saya harapkan itu menjadi:

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

Tetapi sebaliknya ia memakan id dan negara bagian adalah:

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

Apakah perilaku yang diharapkan ini dan apa solusinya untuk memperbarui hanya satu properti dari objek keadaan bersarang?

Acar
sumber

Jawaban:

143

Saya pikir setState()tidak melakukan penggabungan rekursif.

Anda dapat menggunakan nilai dari kondisi saat ini this.state.selecteduntuk membangun negara baru dan kemudian memanggil setState()itu:

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

Saya telah menggunakan fungsi _.extend()fungsi (dari perpustakaan underscore.js) di sini untuk mencegah modifikasi ke selectedbagian negara yang ada dengan membuat salinan yang dangkal.

Solusi lain adalah dengan menulis setStateRecursively()yang menggabungkan rekursif pada negara baru dan kemudian memanggilnya replaceState():

setStateRecursively: function(stateUpdate, callback) {
  var newState = mergeStateRecursively(this.state, stateUpdate);
  this.replaceState(newState, callback);
}
andreypopp
sumber
7
Apakah ini bekerja dengan andal jika Anda melakukannya lebih dari sekali atau apakah ada kemungkinan reaksi akan mengantri pada panggilan replaceState dan yang terakhir akan menang?
edoloughlin
111
Untuk orang yang lebih suka menggunakan extended dan yang juga menggunakan babel, Anda dapat melakukannya this.setState({ selected: { ...this.state.selected, name: 'barfoo' } })yang diterjemahkan kethis.setState({ selected: _extends({}, this.state.selected, { name: 'barfoo' }) });
Pickels
8
@Pickels ini bagus, idiomatis untuk Bereaksi dan tidak memerlukan underscore/ lodash. Tolong, ubah menjadi jawaban sendiri.
ericsoco
14
this.state belum tentu terkini . Anda bisa mendapatkan hasil yang tidak terduga menggunakan metode perluasan. Melewati fungsi pembaruan setStateadalah solusi yang benar.
Strom
1
@ EdwardD'Souza Ini adalah perpustakaan lodash. Ini biasanya dipanggil sebagai garis bawah, dengan cara yang sama jQuery biasanya dipanggil sebagai tanda dolar. (Jadi, ini _.extend ().)
mjk
96

Pembantu ketidaksuburan baru-baru ini ditambahkan ke React.addons, jadi dengan itu, Anda sekarang dapat melakukan sesuatu seperti:

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

Dokumentasi pembantu ketidakmampuan .

pengguna3874611
sumber
7
pembaruan sudah tidak digunakan lagi. facebook.github.io/react/docs/update.html
thedanotto
3
@thedanotto Sepertinya React.addons.update()metode ini sudah usang, tetapi pustaka pengganti github.com/kolodny/immutability-helper berisi update()fungsi yang setara .
Bungle
37

Karena banyak jawaban menggunakan kondisi saat ini sebagai dasar untuk menggabungkan data baru, saya ingin menunjukkan bahwa ini bisa pecah. Status perubahan di-antri, dan jangan langsung mengubah objek status komponen. Karenanya merujuk data keadaan sebelum antrian diproses akan memberi Anda data basi yang tidak mencerminkan perubahan yang tertunda yang Anda buat di setState. Dari dokumen:

setState () tidak segera mengubah this.state tetapi membuat transisi status yang tertunda. Mengakses status ini setelah memanggil metode ini berpotensi mengembalikan nilai yang ada.

Ini berarti menggunakan status "saat ini" sebagai referensi dalam panggilan berikutnya ke setState tidak dapat diandalkan. Sebagai contoh:

  1. Panggilan pertama ke setState, mengantri perubahan ke objek negara
  2. Panggilan kedua ke setState. Status Anda menggunakan objek bersarang, jadi Anda ingin melakukan penggabungan. Sebelum memanggil setState, Anda mendapatkan objek status saat ini. Objek ini tidak mencerminkan perubahan antrian yang dibuat pada panggilan pertama ke setState, di atas, karena itu masih keadaan asli, yang sekarang harus dianggap "basi".
  3. Lakukan penggabungan. Hasilnya adalah keadaan "basi" asli ditambah data baru yang baru Anda atur, perubahan dari panggilan setState awal tidak tercermin. Panggilan setState Anda mengantri pada perubahan kedua ini.
  4. Bereaksi proses antrian. Panggilan setState pertama diproses, memperbarui keadaan. Panggilan setState kedua diproses, memperbarui keadaan. Objek setState kedua sekarang telah menggantikan yang pertama, dan karena data yang Anda miliki ketika membuat panggilan itu basi, data basi yang dimodifikasi dari panggilan kedua ini telah menghancurkan perubahan yang dilakukan pada panggilan pertama, yang hilang.
  5. Ketika antrian kosong, Bereaksi menentukan apakah akan merender dll. Pada titik ini Anda akan merender perubahan yang dibuat dalam panggilan setState kedua, dan seolah-olah panggilan setState pertama tidak pernah terjadi.

Jika Anda perlu menggunakan status saat ini (misalnya untuk menggabungkan data menjadi objek bersarang), setState menerima fungsi sebagai argumen alih-alih objek; fungsi dipanggil setelah setiap pembaruan sebelumnya ke negara, dan melewati negara sebagai argumen - jadi ini dapat digunakan untuk membuat perubahan atom dijamin untuk menghormati perubahan sebelumnya.

bgannonpl
sumber
5
Apakah ada contoh atau lebih dokumen tentang cara melakukan ini? afaik, tidak ada yang memperhitungkan ini.
Josef.B
@ Dennis Coba jawaban saya di bawah ini.
Martin Dawson
Saya tidak melihat di mana masalahnya. Apa yang Anda gambarkan hanya akan terjadi jika Anda mencoba mengakses status "baru" setelah memanggil setState. Itu adalah masalah yang sama sekali berbeda yang tidak ada hubungannya dengan pertanyaan OP. Mengakses negara lama sebelum memanggil setState sehingga Anda dapat membuat objek negara baru tidak akan menjadi masalah, dan sebenarnya itulah yang diharapkan Bereaksi.
nextgentech
@nextgentech "Mengakses negara lama sebelum memanggil setState sehingga Anda dapat membuat objek negara baru tidak akan menjadi masalah" - Anda salah. Kita sedang berbicara tentang kondisi penimpaan berdasarkan this.state, yang mungkin basi (atau lebih tepatnya, pembaruan antrian menunggu) ketika Anda mengaksesnya.
Madbreaks
1
@Madbreak Oke, ya, setelah membaca kembali dokumen resmi, saya melihat bahwa Anda harus menggunakan bentuk fungsi setState untuk memperbarui keadaan berdasarkan pada kondisi sebelumnya. Yang mengatakan, saya tidak pernah menemukan masalah saat ini jika tidak mencoba memanggil setState beberapa kali dalam fungsi yang sama. Terima kasih atas tanggapan yang membuat saya membaca ulang spesifikasi.
nextgentech
10

Saya tidak ingin menginstal perpustakaan lain jadi inilah solusi lain.

Dari pada:

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

Lakukan ini sebagai gantinya:

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

Atau, terima kasih kepada @ icc97 dalam komentarnya, bahkan lebih singkat tetapi tidak terlalu mudah dibaca:

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

Juga, untuk menjadi jelas, jawaban ini tidak melanggar kekhawatiran apa pun yang @bgannonpl sebutkan di atas.

Ryan Shillington
sumber
Suara positif, periksa. Hanya ingin tahu mengapa tidak melakukannya seperti ini? this.setState({ selected: Object.assign(this.state.selected, { name: "Barfoo" }));
Justus Romijn
1
@JustusRomijn Sayangnya Anda akan langsung mengubah keadaan saat ini. Object.assign(a, b)mengambil atribut dari b, masukkan / timpa mereka adan kemudian kembali a. Bereaksi orang menjadi bersemangat jika Anda mengubah keadaan secara langsung.
Ryan Shillington
Persis. Harap dicatat bahwa ini hanya berfungsi untuk copy / assign yang dangkal.
Madbreaks
1
@JustusRomijn Anda dapat melakukan ini sesuai MDN menetapkan dalam satu baris jika Anda menambahkan {}sebagai argumen pertama: this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });. Ini tidak mengubah status awal.
icc97
@ icc97 Bagus! Saya menambahkan itu ke jawaban saya.
Ryan Shillington
8

Mempertahankan status sebelumnya berdasarkan jawaban @bgannonpl:

Contoh Lodash :

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

Untuk memeriksa apakah itu berfungsi dengan baik, Anda dapat menggunakan panggilan fungsi parameter kedua:

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

Saya menggunakan mergekarena extendmembuang properti lain jika tidak.

Contoh Kekebalan Bereaksi :

import update from "react-addons-update";

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

Solusi saya untuk situasi seperti ini adalah menggunakan, seperti jawaban lain yang ditunjukkan, para Pembantu Ketidakmampuan .

Karena mengatur keadaan secara mendalam adalah situasi yang umum, saya telah membuat mixin berikut:

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

Mixin ini termasuk dalam sebagian besar komponen saya dan saya biasanya tidak menggunakan setStatelangsung lagi.

Dengan mixin ini, yang perlu Anda lakukan untuk mencapai efek yang diinginkan adalah memanggil fungsi setStateinDepthdengan cara berikut:

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

Untuk informasi lebih lanjut:

  • Tentang cara kerja mixin di Bereaksi, lihat dokumentasi resmi
  • Pada sintaks parameter yang diteruskan untuk setStateinDepthmelihat dokumentasi Immutability Helpers .
Dorian
sumber
Maaf atas jawaban yang terlambat, tetapi contoh Mixin Anda tampaknya menarik. Namun, jika saya memiliki 2 negara bagian bersarang, mis. This.state.test.one dan this.state.test.two. Benarkah hanya satu di antaranya yang akan diperbarui dan yang lain akan dihapus? Ketika saya memperbarui yang pertama, jika yang kedua saya di negara bagian, itu akan dihapus ...
mamruoc
hy @ mamruoc, this.state.test.twoakan tetap ada di sana setelah Anda memperbarui this.state.test.onemenggunakan setStateInDepth({test: {one: {$set: 'new-value'}}}). Saya menggunakan kode ini di semua komponen saya untuk menyiasati setStatefungsi terbatas React .
Dorian
1
Saya menemukan solusinya. Saya memulai this.state.testsebagai Array. Mengubah ke Objek, menyelesaikannya.
mamruoc
2

Saya menggunakan kelas es6, dan saya berakhir dengan beberapa objek kompleks pada kondisi teratas saya dan berusaha membuat komponen utama saya lebih modular, jadi saya membuat pembungkus kelas sederhana untuk menjaga status pada komponen atas tetapi memungkinkan lebih banyak logika lokal .

Kelas wrapper mengambil fungsi sebagai konstruktornya yang menetapkan properti pada status komponen utama.

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()
    }
}

Kemudian untuk setiap properti kompleks di negara bagian atas, saya membuat satu kelas StateWrapped. Anda dapat mengatur alat peraga default di konstruktor di sini dan mereka akan ditetapkan ketika kelas diinisialisasi, Anda dapat merujuk ke negara lokal untuk nilai dan mengatur negara setempat, merujuk ke fungsi lokal, dan telah melewati rantai:

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"})


}

Jadi komponen tingkat atas saya hanya perlu konstruktor untuk mengatur setiap kelas ke properti negara tingkat atas itu, render sederhana, dan fungsi apa pun yang berkomunikasi lintas-komponen.

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!"})
}

Tampaknya berfungsi cukup baik untuk keperluan saya, ingatlah meskipun Anda tidak dapat mengubah keadaan properti yang Anda tetapkan untuk komponen terbungkus pada komponen tingkat atas karena setiap komponen terbungkus melacak keadaannya sendiri tetapi memperbarui keadaan pada komponen atas setiap kali itu berubah.


sumber
2

Sampai sekarang,

Jika keadaan berikutnya tergantung pada keadaan sebelumnya, sebaiknya gunakan formulir fungsi pembaru, sebagai gantinya:

menurut dokumentasi https://reactjs.org/docs/react-component.html#setstate , menggunakan:

this.setState((prevState) => {
    return {quantity: prevState.quantity + 1};
});
egidioc
sumber
Terima kasih atas kutipan / tautannya, yang hilang dari jawaban lain.
Madbreaks
1

Larutan

Sunting: Solusi ini digunakan untuk menggunakan sintaks spread . Tujuannya adalah membuat objek tanpa referensi prevState, sehingga prevStatetidak akan dimodifikasi. Tetapi dalam penggunaan saya, prevStatekadang-kadang tampaknya dimodifikasi. Jadi, untuk kloning sempurna tanpa efek samping, kami sekarang mengonversi prevStateke JSON, dan kemudian kembali lagi. (Inspirasi untuk menggunakan JSON berasal dari MDN .)

Ingat:

Langkah

  1. Membuat salinan dari properti akar-tingkat dari stateyang Anda ingin perubahan
  2. Mutasikan objek baru ini
  3. Buat objek pembaruan
  4. Kembalikan pembaruan

Langkah 3 dan 4 dapat digabungkan dalam satu baris.

Contoh

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

Contoh sederhana:

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

Ini mengikuti pedoman Bereaksi dengan baik. Berdasarkan jawaban eicksl untuk pertanyaan serupa.

Tim
sumber
1

Solusi ES6

Kami mengatur negara awalnya

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

Kami mengubah properti pada beberapa tingkat objek negara:

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

React state tidak melakukan penggabungan rekursif setStatesementara berharap tidak akan ada pembaruan anggota negara di tempat pada saat yang sama. Anda juga harus menyalin sendiri objek / array yang terlampir (dengan array.slice atau Object.assign) atau menggunakan perpustakaan khusus.

Seperti yang ini. NestedLink secara langsung mendukung penanganan senyawa React state.

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

Juga, tautan ke selectedatau selected.namedapat diteruskan ke mana saja sebagai penyangga tunggal dan dimodifikasi di sana bersama set.

gaperton
sumber
-2

Sudahkah Anda mengatur kondisi awal?

Saya akan menggunakan beberapa kode saya sendiri misalnya:

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

Dalam aplikasi yang sedang saya kerjakan, ini adalah cara saya mengatur dan menggunakan status. Saya yakin setStateAnda dapat mengedit saja status apa pun yang Anda inginkan secara individual. Saya menyebutnya seperti ini:

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

Ingatlah bahwa Anda harus mengatur status di dalam React.createClassfungsi yang Anda panggilgetInitialState

asherrard
sumber
1
mixin apa yang Anda gunakan dalam komponen Anda? Ini tidak berfungsi untuk saya
Sachin
-3

Saya menggunakan tmp var untuk berubah.

changeTheme(v) {
    let tmp = this.state.tableData
    tmp.theme = v
    this.setState({
        tableData : tmp
    })
}
hkongm
sumber
2
Di sini tmp.theme = v sedang bermutasi. Jadi ini bukan cara yang disarankan.
almoraleslopez
1
let original = {a:1}; let tmp = original; tmp.a = 5; // original is now === Object {a: 5} Jangan lakukan ini, bahkan jika itu belum memberi Anda masalah.
Chris