Để sửa đổi các đối tượng / biến được lồng nhau sâu trong trạng thái của React, thông thường có ba phương thức được sử dụng: vanilla JavaScript Object.assign
, trình trợ giúp bất biến và cloneDeep
từ Lodash .
Ngoài ra còn có rất nhiều libs bên thứ ba ít phổ biến khác để đạt được điều này, nhưng trong câu trả lời này, tôi sẽ chỉ đề cập đến ba tùy chọn này. Ngoài ra, một số phương thức JavaScript vanilla bổ sung tồn tại, như trải rộng mảng, (ví dụ xem câu trả lời của @ mpen), nhưng chúng không trực quan, dễ sử dụng và có khả năng xử lý tất cả các tình huống thao tác trạng thái.
Như đã được chỉ ra vô số lần trong các bình luận được bình chọn hàng đầu cho các câu trả lời, mà các tác giả đề xuất một sự đột biến trực tiếp của nhà nước: chỉ cần không làm điều đó . Đây là một mô hình chống React phổ biến, chắc chắn sẽ dẫn đến hậu quả không mong muốn. Học đúng cách.
Hãy so sánh ba phương pháp được sử dụng rộng rãi.
Cho cấu trúc đối tượng trạng thái này:
state = {
outer: {
inner: 'initial value'
}
}
Bạn có thể sử dụng các phương pháp sau để cập nhật inner
giá trị của trường bên trong nhất mà không ảnh hưởng đến phần còn lại của trạng thái.
1. Object.assign của Vanilla JavaScript
const App = () => {
const [outer, setOuter] = React.useState({ inner: 'initial value' })
React.useEffect(() => {
console.log('Before the shallow copying:', outer.inner) // initial value
const newOuter = Object.assign({}, outer, { inner: 'updated value' })
console.log('After the shallow copy is taken, the value in the state is still:', outer.inner) // initial value
setOuter(newOuter)
}, [])
console.log('In render:', outer.inner)
return (
<section>Inner property: <i>{outer.inner}</i></section>
)
}
ReactDOM.render(
<App />,
document.getElementById('react')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
<main id="react"></main>
Hãy nhớ rằng Object.assign sẽ không thực hiện nhân bản sâu , vì nó chỉ sao chép các giá trị thuộc tính và đó là lý do tại sao cái được gọi là sao chép nông (xem bình luận).
Để làm việc này, chúng ta chỉ nên thao tác các thuộc tính của kiểu nguyên thủy ( outer.inner
), đó là chuỗi, số, booleans.
Trong ví dụ này, chúng ta đang tạo một hằng số mới ( const newOuter...
), bằng cách sử dụng Object.assign
, tạo ra một đối tượng trống ( {}
), sao chép outer
đối tượng ( { inner: 'initial value' }
) vào đó và sau đó sao chép một đối tượng khác { inner: 'updated value' }
lên trên nó.
Bằng cách này, cuối cùng, newOuter
hằng số mới được tạo sẽ giữ một giá trị { inner: 'updated value' }
kể từ khi thuộc inner
tính bị ghi đè. Đây newOuter
là một đối tượng hoàn toàn mới, không được liên kết với đối tượng ở trạng thái, vì vậy nó có thể bị thay đổi khi cần thiết và trạng thái sẽ giữ nguyên và không thay đổi cho đến khi lệnh cập nhật được chạy.
Phần cuối cùng là sử dụng setOuter()
setter để thay thế bản gốc outer
trong trạng thái bằng một newOuter
đối tượng mới được tạo (chỉ giá trị sẽ thay đổi, tên thuộc tính outer
sẽ không).
Bây giờ hãy tưởng tượng chúng ta có một trạng thái sâu sắc hơn như thế state = { outer: { inner: { innerMost: 'initial value' } } }
. Chúng ta có thể cố gắng tạo newOuter
đối tượng và điền vào outer
nội dung từ trạng thái, nhưng Object.assign
sẽ không thể sao chép innerMost
giá trị của newOuter
đối tượng mới được tạo này vì innerMost
được lồng quá sâu.
Bạn vẫn có thể sao chép inner
, như trong ví dụ trên, nhưng vì bây giờ nó là một đối tượng và không phải là nguyên thủy, nên tham chiếu từ newOuter.inner
sẽ được sao chép vào outer.inner
thay vào đó, điều đó có nghĩa là chúng ta sẽ kết thúc với newOuter
đối tượng cục bộ trực tiếp gắn với đối tượng ở trạng thái .
Điều đó có nghĩa là trong trường hợp này các đột biến của cục bộ được tạo newOuter.inner
sẽ ảnh hưởng trực tiếp đến outer.inner
đối tượng (ở trạng thái), vì thực tế chúng đã trở thành cùng một thứ (trong bộ nhớ của máy tính).
Object.assign
do đó sẽ chỉ hoạt động nếu bạn có cấu trúc trạng thái sâu một cấp tương đối đơn giản với các thành viên trong cùng giữ các giá trị của kiểu nguyên thủy.
Nếu bạn có các đối tượng sâu hơn (cấp 2 trở lên), bạn nên cập nhật, không sử dụng Object.assign
. Bạn có nguy cơ đột biến trạng thái trực tiếp.
2. Bản sao của Lodash
const App = () => {
const [outer, setOuter] = React.useState({ inner: 'initial value' })
React.useEffect(() => {
console.log('Before the deep cloning:', outer.inner) // initial value
const newOuter = _.cloneDeep(outer) // cloneDeep() is coming from the Lodash lib
newOuter.inner = 'updated value'
console.log('After the deeply cloned object is modified, the value in the state is still:', outer.inner) // initial value
setOuter(newOuter)
}, [])
console.log('In render:', outer.inner)
return (
<section>Inner property: <i>{outer.inner}</i></section>
)
}
ReactDOM.render(
<App />,
document.getElementById('react')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
<main id="react"></main>
ClashDeep của Lodash là cách đơn giản hơn để sử dụng. Nó thực hiện một nhân bản sâu , vì vậy nó là một tùy chọn mạnh mẽ, nếu bạn có một trạng thái khá phức tạp với các đối tượng đa cấp hoặc mảng bên trong. Chỉ cloneDeep()
là tài sản nhà nước cấp cao nhất, biến đổi phần nhân bản theo bất cứ cách nào bạn muốn, và setOuter()
nó trở lại trạng thái.
3. bất biến-người trợ giúp
const App = () => {
const [outer, setOuter] = React.useState({ inner: 'initial value' })
React.useEffect(() => {
const update = immutabilityHelper
console.log('Before the deep cloning and updating:', outer.inner) // initial value
const newOuter = update(outer, { inner: { $set: 'updated value' } })
console.log('After the cloning and updating, the value in the state is still:', outer.inner) // initial value
setOuter(newOuter)
}, [])
console.log('In render:', outer.inner)
return (
<section>Inner property: <i>{outer.inner}</i></section>
)
}
ReactDOM.render(
<App />,
document.getElementById('react')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
<script src="https://wzrd.in/standalone/immutability-helper@3.0.0"></script>
<main id="react"></main>
immutability-helper
mất nó vào một cấp độ hoàn toàn mới, và điều thú vị về nó là nó không chỉ có thể $set
đánh giá cao đến các mục nhà nước, mà còn $push
, $splice
, $merge
(vv) cho họ. Dưới đây là danh sách các lệnh có sẵn.
Ghi chú bên
Một lần nữa, hãy nhớ rằng, setOuter
chỉ sửa đổi các thuộc tính cấp đầu tiên của đối tượng trạng thái ( outer
trong các ví dụ này), chứ không phải lồng nhau sâu ( outer.inner
). Nếu nó hành xử theo một cách khác, câu hỏi này sẽ không tồn tại.
Cái nào phù hợp với dự án của bạn?
Nếu bạn không muốn hoặc không thể sử dụng các phụ thuộc bên ngoài và có cấu trúc trạng thái đơn giản , hãy tuân thủ Object.assign
.
Nếu bạn thao túng một trạng thái lớn và / hoặc phức tạp , thì Lodash cloneDeep
là một lựa chọn sáng suốt.
Nếu bạn cần các khả năng nâng cao , tức là nếu cấu trúc trạng thái của bạn phức tạp và bạn cần thực hiện tất cả các loại hoạt động trên nó, hãy thử immutability-helper
, đó là một công cụ rất tiên tiến có thể được sử dụng để thao tác trạng thái.
... Hoặc, bạn có thực sự cần phải làm điều này không?
Nếu bạn giữ một dữ liệu phức tạp ở trạng thái React, có lẽ đây là thời điểm tốt để suy nghĩ về các cách xử lý khác. Đặt một đối tượng trạng thái phức tạp ngay trong các thành phần React không phải là một hoạt động đơn giản và tôi thực sự khuyên bạn nên suy nghĩ về các phương pháp khác nhau.
Rất có thể bạn nên tắt dữ liệu phức tạp của mình trong một cửa hàng Redux, đặt nó ở đó bằng cách sử dụng các bộ giảm tốc và / hoặc sagas và truy cập nó bằng các bộ chọn.