this.setState không hợp nhất các trạng thái như tôi mong đợi


160

Tôi có trạng thái sau:

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

Sau đó tôi cập nhật trạng thái:

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

setStateđược cho là hợp nhất nên tôi mong đợi nó sẽ là:

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

Nhưng thay vào đó, nó ăn id và trạng thái là:

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

Đây có phải là hành vi dự kiến ​​không và giải pháp nào chỉ cập nhật một thuộc tính của một đối tượng trạng thái lồng nhau?

Câu trả lời:


143

Tôi nghĩ setState()không hợp nhất đệ quy.

Bạn có thể sử dụng giá trị của trạng thái hiện tại this.state.selectedđể xây dựng trạng thái mới và sau đó gọi setState()vào đó:

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

Tôi đã sử dụng chức năng chức _.extend()năng (từ thư viện underscore.js) ở đây để ngăn sửa đổi selectedphần hiện có của trạng thái bằng cách tạo một bản sao nông của nó.

Một giải pháp khác là viết setStateRecursively()kết hợp đệ quy trên một trạng thái mới và sau đó gọi replaceState()nó:

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

7
Công việc này có đáng tin cậy không nếu bạn thực hiện nhiều lần hoặc có khả năng phản ứng sẽ xếp hàng các cuộc gọi thay thế và cuộc gọi cuối cùng sẽ giành chiến thắng?
edoloughlin

111
Đối với những người thích sử dụng tiện ích mở rộng và cũng đang sử dụng babel, bạn có thể thực hiện this.setState({ selected: { ...this.state.selected, name: 'barfoo' } })được dịch sangthis.setState({ selected: _extends({}, this.state.selected, { name: 'barfoo' }) });
Pickels

8
@Pickels điều này thật tuyệt, thành ngữ độc đáo cho React và không yêu cầu underscore/ lodash. Xin vui lòng quay nó vào câu trả lời của riêng mình.
ericsoco

14
this.state không nhất thiết phải cập nhật . Bạn có thể nhận được kết quả bất ngờ bằng cách sử dụng phương pháp mở rộng. Truyền một chức năng cập nhật setStatelà giải pháp chính xác.
Strom

1
@ EdwardD'Souza Đó là thư viện lodash. Nó thường được gọi là dấu gạch dưới, giống như cách jQuery thường được gọi là đô la. (Vì vậy, đó là _.extend ().)
mjk

96

Những người trợ giúp bất biến gần đây đã được thêm vào React.addons, vì vậy, với điều đó, giờ đây bạn có thể làm một cái gì đó như:

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

Tài liệu trợ giúp bất biến .


7
cập nhật bị phản đối facebook.github.io/react/docs/update.html
thedanotto

3
@thedanotto Có vẻ như React.addons.update()phương thức này không được dùng nữa, nhưng thư viện thay thế github.com/kolodny/immutability-helper chứa một update()hàm tương đương .
Bungle

37

Vì nhiều câu trả lời sử dụng trạng thái hiện tại làm cơ sở để hợp nhất trong dữ liệu mới, tôi muốn chỉ ra rằng điều này có thể phá vỡ. Các thay đổi trạng thái được xếp hàng và không sửa đổi ngay lập tức đối tượng trạng thái của thành phần. Do đó, việc tham chiếu dữ liệu trạng thái trước khi hàng đợi được xử lý sẽ cung cấp cho bạn dữ liệu cũ không phản ánh các thay đổi đang chờ xử lý mà bạn đã thực hiện trong setState. Từ các tài liệu:

setState () không ngay lập tức biến đổi this.state nhưng tạo ra một chuyển đổi trạng thái đang chờ xử lý. Truy cập this.state sau khi gọi phương thức này có khả năng trả về giá trị hiện có.

Điều này có nghĩa là sử dụng trạng thái "hiện tại" làm tham chiếu trong các cuộc gọi tiếp theo tới setState là không đáng tin cậy. Ví dụ:

  1. Cuộc gọi đầu tiên đến setState, xếp hàng thay đổi đối tượng trạng thái
  2. Cuộc gọi thứ hai đến setState. Trạng thái của bạn sử dụng các đối tượng lồng nhau, vì vậy bạn muốn thực hiện hợp nhất. Trước khi gọi setState, bạn nhận được đối tượng trạng thái hiện tại. Đối tượng này không phản ánh các thay đổi được xếp hàng được thực hiện trong lệnh gọi đầu tiên đến setState, ở trên, vì đây vẫn là trạng thái ban đầu, hiện được coi là "cũ".
  3. Thực hiện hợp nhất. Kết quả là trạng thái "cũ" ban đầu cộng với dữ liệu mới bạn vừa đặt, những thay đổi từ cuộc gọi setState ban đầu không được phản ánh. Cuộc gọi setState của bạn xếp hàng thay đổi thứ hai này.
  4. Phản ứng quá trình xếp hàng. Cuộc gọi setState đầu tiên được xử lý, cập nhật trạng thái. Cuộc gọi setState thứ hai được xử lý, cập nhật trạng thái. Đối tượng thứ hai của setState hiện đã thay thế đối tượng thứ nhất và vì dữ liệu bạn có khi thực hiện cuộc gọi đó đã cũ, dữ liệu cũ đã sửa đổi từ cuộc gọi thứ hai này đã ghi đè các thay đổi được thực hiện trong cuộc gọi đầu tiên, bị mất.
  5. Khi hàng đợi trống, React xác định có hiển thị hay không. Tại thời điểm này, bạn sẽ kết xuất các thay đổi được thực hiện trong lệnh gọi setState thứ hai và sẽ như thể cuộc gọi setState đầu tiên không bao giờ xảy ra.

Nếu bạn cần sử dụng trạng thái hiện tại (ví dụ: để hợp nhất dữ liệu vào một đối tượng lồng nhau), setState thay thế chấp nhận một hàm làm đối số thay vì đối tượng; hàm được gọi sau bất kỳ cập nhật nào trước đó sang trạng thái và chuyển trạng thái làm đối số - vì vậy điều này có thể được sử dụng để thực hiện các thay đổi nguyên tử được đảm bảo để tôn trọng các thay đổi trước đó.


5
Có một ví dụ hoặc nhiều tài liệu về cách làm điều này? afaik, không ai tính đến điều này.
Josef.B

@Dennis Hãy thử câu trả lời của tôi dưới đây.
Martin Dawson

Tôi không thấy vấn đề ở đâu. Những gì bạn đang mô tả sẽ chỉ xảy ra nếu bạn cố gắng truy cập trạng thái "mới" sau khi gọi setState. Đó là một vấn đề hoàn toàn khác mà không liên quan gì đến câu hỏi của OP. Truy cập trạng thái cũ trước khi gọi setState để bạn có thể xây dựng một đối tượng trạng thái mới sẽ không bao giờ là vấn đề và thực tế đó chính xác là những gì React mong đợi.
nextgentech

@nextgentech "Truy cập trạng thái cũ trước khi gọi setState để bạn có thể xây dựng một đối tượng trạng thái mới sẽ không bao giờ là vấn đề" - bạn đã nhầm. Chúng ta đang nói về trạng thái ghi đè dựa trên this.state, có thể cũ (hoặc đúng hơn là cập nhật hàng đợi đang chờ xử lý) khi bạn truy cập nó.
Madbreaks

1
@Madbreaks Ok, vâng, khi đọc lại các tài liệu chính thức tôi thấy rằng nó được chỉ định rằng bạn phải sử dụng dạng hàm của setState để cập nhật trạng thái đáng tin cậy dựa trên trạng thái trước đó. Điều đó nói rằng, tôi chưa bao giờ gặp phải một vấn đề cho đến nay nếu không cố gắng gọi setState nhiều lần trong cùng một chức năng. Cảm ơn câu trả lời đã khiến tôi đọc lại thông số kỹ thuật.
nextgentech

10

Tôi không muốn cài đặt một thư viện khác vì vậy đây là một giải pháp khác.

Thay vì:

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

Thay vào đó, hãy làm điều này:

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

Hoặc, nhờ @ icc97 trong các bình luận, thậm chí ngắn gọn hơn nhưng có thể đọc được ít hơn:

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

Ngoài ra, để rõ ràng, câu trả lời này không vi phạm bất kỳ mối quan tâm nào mà @bgannonpl đã đề cập ở trên.


Upvote, kiểm tra. Chỉ tự hỏi tại sao không làm điều đó như thế này? this.setState({ selected: Object.assign(this.state.selected, { name: "Barfoo" }));
Justus Romijn

1
@JustusRomijn Thật không may, sau đó bạn sẽ sửa đổi trạng thái hiện tại trực tiếp. Object.assign(a, b)lấy các thuộc tính từ b, chèn / ghi đè chúng vào avà sau đó trả về a. Phản ứng mọi người trở nên khó chịu nếu bạn sửa đổi trạng thái trực tiếp.
Ryan Shillington

Chính xác. Chỉ cần lưu ý rằng điều này chỉ hoạt động cho bản sao / gán nông.
Madbreaks

1
@JustusRomijn Bạn có thể làm điều này theo MDN được gán trong một dòng nếu bạn thêm {}làm đối số đầu tiên : this.setState({ selected: Object.assign({}, this.state.selected, { name: "Barfoo" }) });. Điều này không làm thay đổi trạng thái ban đầu.
icc97

@ icc97 Đẹp! Tôi đã thêm nó vào câu trả lời của tôi.
Ryan Shillington

8

Giữ nguyên trạng thái trước dựa trên câu trả lời @bgannonpl:

Lodash dụ:

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

Để kiểm tra xem nó có hoạt động tốt không, bạn có thể sử dụng hàm gọi lại tham số thứ hai:

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

Tôi đã sử dụng mergeextend loại bỏ các thuộc tính khác.

Ví dụ về tính bất biến của React :

import update from "react-addons-update";

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

2

Giải pháp của tôi cho loại tình huống này là sử dụng, giống như một câu trả lời khác được chỉ ra, những người trợ giúp bất biến .

Vì đặt trạng thái theo chiều sâu là một tình huống phổ biến, tôi đã tạo ra hỗn hợp folowing:

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

Mixin này được bao gồm trong hầu hết các thành phần của tôi và tôi thường không sử dụng setStatetrực tiếp nữa.

Với mixin này, tất cả những gì bạn cần làm để đạt được hiệu quả mong muốn là gọi hàm setStateinDepththeo cách sau:

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

Để biết thêm thông tin:

  • Về cách mixins hoạt động trong React, xem tài liệu chính thức
  • Trên cú pháp của tham số được truyền để setStateinDepthxem tài liệu Trợ giúp bất biến .

Xin lỗi vì đã trả lời trễ, nhưng ví dụ Mixin của bạn có vẻ thú vị. Tuy nhiên, nếu tôi có 2 trạng thái lồng nhau, ví dụ this.state.test.one và this.state.test.two. Có đúng là chỉ một trong số này sẽ được cập nhật và cái còn lại sẽ bị xóa không? Khi tôi cập nhật cái đầu tiên, nếu cái thứ hai tôi ở trạng thái, nó sẽ bị xóa ...
mamruoc

@mamruoc hy, this.state.test.twovẫn sẽ ở đó sau khi bạn cập nhật this.state.test.onesử dụng setStateInDepth({test: {one: {$set: 'new-value'}}}). Tôi sử dụng mã này trong tất cả các thành phần của mình để khắc phục setStatechức năng giới hạn của React .
Dorian

1
Tôi tìm thấy giải pháp. Tôi đã bắt đầu this.state.testnhư là một mảng. Thay đổi thành Đối tượng, giải quyết nó.
mamruoc

2

Tôi đang sử dụng các lớp es6 và tôi đã kết thúc với một số đối tượng phức tạp ở trạng thái hàng đầu của mình và đang cố gắng làm cho thành phần chính của mình trở nên mô đun hơn, vì vậy tôi đã tạo một trình bao bọc lớp đơn giản để giữ trạng thái trên thành phần trên cùng nhưng cho phép logic cục bộ hơn .

Lớp trình bao bọc lấy một hàm làm hàm tạo của nó để đặt một thuộc tính trên trạng thái thành phần chính.

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

Sau đó, đối với mỗi thuộc tính phức tạp ở trạng thái trên cùng, tôi tạo một lớp StateWrapping. Bạn có thể đặt các đạo cụ mặc định trong hàm tạo ở đây và chúng sẽ được đặt khi lớp được khởi tạo, bạn có thể tham khảo trạng thái cục bộ cho các giá trị và đặt trạng thái cục bộ, tham khảo các hàm cục bộ và chuyển nó qua chuỗi:

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


}

Vì vậy, thành phần cấp cao nhất của tôi chỉ cần hàm tạo để đặt mỗi lớp thành thuộc tính trạng thái cấp cao nhất, kết xuất đơn giản và bất kỳ chức năng nào giao tiếp thành phần chéo.

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

Có vẻ như hoạt động khá tốt cho mục đích của tôi, hãy nhớ rằng bạn không thể thay đổi trạng thái của các thuộc tính bạn gán cho các thành phần được bọc ở thành phần cấp cao nhất vì mỗi thành phần được bọc đang theo dõi trạng thái của chính nó nhưng cập nhật trạng thái trên thành phần trên cùng mỗi lần nó thay đổi.


2

Như ngay bây giờ,

Nếu trạng thái tiếp theo phụ thuộc vào trạng thái trước đó, chúng tôi khuyên bạn nên sử dụng biểu mẫu chức năng cập nhật, thay vào đó:

theo tài liệu https://reactjs.org/docs/react-component.html#setstate , sử dụng:

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

Cảm ơn các trích dẫn / liên kết, đã bị thiếu trong các câu trả lời khác.
Madbreaks

1

Giải pháp

Chỉnh sửa: Giải pháp này được sử dụng để sử dụng cú pháp lây lan . Mục tiêu là tạo ra một đối tượng mà không có bất kỳ tham chiếu nào prevState, do đó prevStatesẽ không được sửa đổi. Nhưng trong cách sử dụng của tôi, prevStateđôi khi dường như được sửa đổi. Vì vậy, để nhân bản hoàn hảo mà không có tác dụng phụ, bây giờ chúng tôi chuyển đổi prevStatesang JSON và sau đó quay lại. (Cảm hứng sử dụng JSON đến từ MDN .)

Nhớ lại:

Các bước

  1. Tạo một bản sao của tài sản gốc cấp của statemà bạn muốn thay đổi
  2. Đột biến đối tượng mới này
  3. Tạo một đối tượng cập nhật
  4. Trả lại bản cập nhật

Bước 3 và 4 có thể được kết hợp trên một dòng.

Thí dụ

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

Ví dụ đơn giản:

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

Điều này tuân theo các hướng dẫn React độc đáo. Dựa trên câu trả lời của eicksl cho một câu hỏi tương tự.


1

Giải pháp ES6

Chúng tôi thiết lập trạng thái ban đầu

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

Chúng tôi đang thay đổi một tài sản ở một số cấp độ của đối tượng nhà nước:

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

0

Trạng thái React không thực hiện hợp nhất đệ quy trong setStatekhi hy vọng rằng sẽ không có cập nhật thành viên nhà nước tại chỗ cùng một lúc. Bạn phải tự sao chép các đối tượng / mảng kèm theo (với mảng.slice hoặc Object.assign) hoặc sử dụng thư viện chuyên dụng.

Giống như cái này. NestedLink trực tiếp hỗ trợ xử lý trạng thái React ghép.

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

Ngoài ra, liên kết đến selectedhoặc selected.namecó thể được truyền đi khắp nơi dưới dạng một chỗ dựa duy nhất và được sửa đổi ở đó với set.


-2

bạn đã thiết lập trạng thái ban đầu chưa?

Tôi sẽ sử dụng một số mã của riêng tôi chẳng hạn:

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

Trong một ứng dụng tôi đang làm việc, đây là cách tôi đã thiết lập và sử dụng trạng thái. Tôi tin vào setStatebạn sau đó có thể chỉnh sửa bất kỳ trạng thái nào bạn muốn cá nhân tôi đã gọi nó như vậy:

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

Hãy nhớ rằng bạn phải đặt trạng thái trong React.createClasshàm mà bạn đã gọigetInitialState


1
mixin nào bạn đang sử dụng trong thành phần của bạn? Điều này không hiệu quả với tôi
Sachin

-3

Tôi sử dụng var tmp để thay đổi.

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

2
Ở đây tmp.theme = v là trạng thái đột biến. Vì vậy, đây không phải là cách được đề nghị.
almoraleslopez

1
let original = {a:1}; let tmp = original; tmp.a = 5; // original is now === Object {a: 5} Đừng làm điều này, ngay cả khi nó chưa đưa ra vấn đề cho bạn.
Chris
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.