Bảo tồn và đặt lại State
State được cô lập giữa các component. React theo dõi state nào thuộc về component nào dựa trên vị trí của chúng trong cây UI. Bạn có thể kiểm soát khi nào bảo tồn state và khi nào đặt lại nó giữa các lần render.
Bạn sẽ được học
- Khi nào React chọn bảo tồn hoặc đặt lại state
- Cách buộc React đặt lại state của component
- Cách keys và types ảnh hưởng đến việc state có được bảo tồn hay không
State được liên kết với một vị trí trong cây render
React xây dựng cây render cho cấu trúc component trong UI của bạn.
Khi bạn trao state cho một component, bạn có thể nghĩ rằng state “sống” bên trong component đó. Nhưng state thực sự được giữ bên trong React. React liên kết từng phần state mà nó đang giữ với component chính xác theo vị trí mà component đó nằm trong cây render.
Ở đây, chỉ có một thẻ JSX <Counter />
, nhưng nó được render ở hai vị trí khác nhau:
import { useState } from 'react'; export default function App() { const counter = <Counter />; return ( <div> {counter} {counter} </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Đây là cách chúng trông như một cây:


Cây React
Đây là hai counter riêng biệt vì mỗi cái được render ở vị trí riêng của nó trong cây. Bạn thường không cần phải suy nghĩ về những vị trí này để sử dụng React, nhưng hiểu cách nó hoạt động có thể hữu ích.
Trong React, mỗi component trên màn hình có state hoàn toàn cô lập. Ví dụ, nếu bạn render hai component Counter
cạnh nhau, mỗi component sẽ có state score
và hover
riêng độc lập.
Thử click vào cả hai counter và để ý rằng chúng không ảnh hưởng lẫn nhau:
import { useState } from 'react'; export default function App() { return ( <div> <Counter /> <Counter /> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Như bạn có thể thấy, khi một counter được cập nhật, chỉ state của component đó được cập nhật:


Cập nhật state
React sẽ giữ state xung quanh miễn là bạn render cùng một component ở cùng vị trí trong cây. Để thấy điều này, hãy tăng cả hai counter, sau đó xóa component thứ hai bằng cách bỏ check hộp “Render the second counter”, và sau đó thêm lại bằng cách tick lại:
import { useState } from 'react'; export default function App() { const [showB, setShowB] = useState(true); return ( <div> <Counter /> {showB && <Counter />} <label> <input type="checkbox" checked={showB} onChange={e => { setShowB(e.target.checked) }} /> Render the second counter </label> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Chú ý cách ngay khi bạn ngừng render counter thứ hai, state của nó biến mất hoàn toàn. Đó là vì khi React xóa một component, nó phá hủy state của component đó.


Xóa một component
Khi bạn tick “Render the second counter”, một Counter
thứ hai và state của nó được khởi tạo từ đầu (score = 0
) và thêm vào DOM.


Thêm một component
React bảo tồn state của một component miễn là nó đang được render ở vị trí của nó trong cây UI. Nếu nó bị xóa, hoặc một component khác được render ở cùng vị trí, React sẽ loại bỏ state của nó.
Cùng component ở cùng vị trí bảo tồn state
Trong ví dụ này, có hai thẻ <Counter />
khác nhau:
import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return ( <div> {isFancy ? ( <Counter isFancy={true} /> ) : ( <Counter isFancy={false} /> )} <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> Use fancy styling </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Khi bạn tick hoặc bỏ tick checkbox, state counter không bị đặt lại. Dù isFancy
là true
hay false
, bạn luôn có một <Counter />
làm con đầu tiên của div
được trả về từ component root App
:


Cập nhật state của App
không đặt lại Counter
vì Counter
vẫn ở cùng vị trí
Đó là cùng một component ở cùng vị trí, vì vậy từ góc nhìn của React, đó là cùng một counter.
Các component khác nhau ở cùng vị trí đặt lại state
Trong ví dụ này, việc tick vào checkbox sẽ thay thế <Counter>
bằng một <p>
:
import { useState } from 'react'; export default function App() { const [isPaused, setIsPaused] = useState(false); return ( <div> {isPaused ? ( <p>See you later!</p> ) : ( <Counter /> )} <label> <input type="checkbox" checked={isPaused} onChange={e => { setIsPaused(e.target.checked) }} /> Take a break </label> </div> ); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Ở đây, bạn chuyển đổi giữa các loại component khác nhau ở cùng vị trí. Ban đầu, con đầu tiên của <div>
chứa một Counter
. Nhưng khi bạn thay thế bằng một p
, React đã xóa Counter
khỏi cây UI và phá hủy state của nó.


Khi Counter
thay đổi thành p
, Counter
bị xóa và p
được thêm vào


Khi chuyển đổi trở lại, p
bị xóa và Counter
được thêm vào
Ngoài ra, khi bạn render một component khác ở cùng vị trí, nó đặt lại state của toàn bộ cây con. Để thấy cách hoạt động, hãy tăng counter rồi tick vào checkbox:
import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return ( <div> {isFancy ? ( <div> <Counter isFancy={true} /> </div> ) : ( <section> <Counter isFancy={false} /> </section> )} <label> <input type="checkbox" checked={isFancy} onChange={e => { setIsFancy(e.target.checked) }} /> Use fancy styling </label> </div> ); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
State counter bị đặt lại khi bạn click vào checkbox. Mặc dù bạn render một Counter
, con đầu tiên của div
thay đổi từ section
thành div
. Khi section
con bị xóa khỏi DOM, toàn bộ cây bên dưới nó (bao gồm Counter
và state của nó) cũng bị phá hủy.


Khi section
thay đổi thành div
, section
bị xóa và div
mới được thêm vào


Khi chuyển đổi trở lại, div
bị xóa và section
mới được thêm vào
Theo nguyên tắc chung, nếu bạn muốn bảo tồn state giữa các lần render, cấu trúc cây của bạn cần “khớp” từ lần render này sang lần render khác. Nếu cấu trúc khác nhau, state sẽ bị hủy vì React hủy state khi nó xóa một component khỏi cây.
Đặt lại state ở cùng vị trí
Theo mặc định, React bảo tồn state của một component khi nó ở cùng vị trí. Thường thì, đây chính xác là điều bạn muốn, vì vậy nó hợp lý như hành vi mặc định. Nhưng đôi khi, bạn có thể muốn đặt lại state của một component. Hãy xem xét ứng dụng này cho phép hai người chơi theo dõi điểm số của họ trong mỗi lượt:
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA ? ( <Counter person="Taylor" /> ) : ( <Counter person="Sarah" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Next player! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{person}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Hiện tại, khi bạn thay đổi người chơi, điểm số được bảo tồn. Hai Counter
xuất hiện ở cùng vị trí, vì vậy React coi chúng là cùng một Counter
có prop person
đã thay đổi.
Nhưng về mặt khái niệm, trong ứng dụng này chúng nên là hai counter riêng biệt. Chúng có thể xuất hiện ở cùng vị trí trong UI, nhưng một cái là counter cho Taylor, và cái khác là counter cho Sarah.
Có hai cách để đặt lại state khi chuyển đổi giữa chúng:
- Render các component ở các vị trí khác nhau
- Trao cho mỗi component một danh tính rõ ràng với
key
Lựa chọn 1: Render một component ở các vị trí khác nhau
Nếu bạn muốn hai Counter
này độc lập, bạn có thể render chúng ở hai vị trí khác nhau:
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA && <Counter person="Taylor" /> } {!isPlayerA && <Counter person="Sarah" /> } <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Next player! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{person}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
- Ban đầu,
isPlayerA
làtrue
. Vậy vị trí đầu tiên chứa stateCounter
, và vị trí thứ hai trống. - Khi bạn click button “Next player”, vị trí đầu tiên được xóa nhưng vị trí thứ hai giờ chứa một
Counter
.


State ban đầu


Click “next”


Click “next” lại
State của mỗi Counter
bị hủy mỗi khi nó bị xóa khỏi DOM. Đây là lý do tại sao chúng đặt lại mỗi lần bạn click button.
Giải pháp này thuận tiện khi bạn chỉ có một vài component độc lập được render ở cùng chỗ. Trong ví dụ này, bạn chỉ có hai, vì vậy việc render cả hai riêng biệt trong JSX không phiền hà.
Lựa chọn 2: Đặt lại state với một key
Ngoài ra còn có một cách khác, tổng quát hơn, để đặt lại state của một component.
Bạn có thể đã thấy key
khi render danh sách. Keys không chỉ dành cho danh sách! Bạn có thể sử dụng keys để làm cho React phân biệt giữa bất kỳ component nào. Theo mặc định, React sử dụng thứ tự trong parent (“counter đầu tiên”, “counter thứ hai”) để phân biệt giữa các component. Nhưng keys cho phép bạn nói với React rằng đây không chỉ là counter đầu tiên, hoặc counter thứ hai, mà là một counter cụ thể—ví dụ, counter của Taylor. Bằng cách này, React sẽ biết counter của Taylor bất cứ nơi nào nó xuất hiện trong cây!
Trong ví dụ này, hai <Counter />
không chia sẻ state mặc dù chúng xuất hiện ở cùng vị trí trong JSX:
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA ? ( <Counter key="Taylor" person="Taylor" /> ) : ( <Counter key="Sarah" person="Sarah" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Next player! </button> </div> ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return ( <div className={className} onPointerEnter={() => setHover(true)} onPointerLeave={() => setHover(false)} > <h1>{person}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Việc chuyển đổi giữa Taylor và Sarah không bảo tồn state. Điều này vì bạn đã trao cho chúng các key
khác nhau:
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
Chỉ định một key
nói với React sử dụng chính key
đó như một phần của vị trí, thay vì thứ tự của chúng trong parent. Đây là lý do tại sao, mặc dù bạn render chúng ở cùng vị trí trong JSX, React coi chúng là hai counter khác nhau, và vì vậy chúng sẽ không bao giờ chia sẻ state. Mỗi khi một counter xuất hiện trên màn hình, state của nó được tạo. Mỗi khi nó bị xóa, state của nó bị hủy. Việc chuyển đổi giữa chúng đặt lại state của chúng lặp đi lặp lại.
Đặt lại một form với một key
Đặt lại state với một key đặc biệt hữu ích khi xử lý forms.
Trong ứng dụng chat này, component <Chat>
chứa state text input:
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat contact={to} /> </div> ) } const contacts = [ { id: 0, name: 'Taylor', email: 'taylor@mail.com' }, { id: 1, name: 'Alice', email: 'alice@mail.com' }, { id: 2, name: 'Bob', email: 'bob@mail.com' } ];
Thử nhập một cái gì đó vào input, sau đó nhấn “Alice” hoặc “Bob” để chọn người nhận khác. Bạn sẽ nhận thấy rằng state input được bảo tồn vì <Chat>
được render ở cùng vị trí trong cây.
Trong nhiều ứng dụng, đây có thể là hành vi mong muốn, nhưng không phải trong ứng dụng chat! Bạn không muốn để người dùng gửi tin nhắn mà họ đã gõ đến người sai vì click nhầm. Để sửa điều này, hãy thêm một key
:
<Chat key={to.id} contact={to} />
Điều này đảm bảo rằng khi bạn chọn một người nhận khác, component Chat
sẽ được tạo lại từ đầu, bao gồm bất kỳ state nào trong cây bên dưới nó. React cũng sẽ tạo lại các DOM elements thay vì tái sử dụng chúng.
Giờ việc chuyển đổi người nhận luôn xóa trường text:
import { useState } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; export default function Messenger() { const [to, setTo] = useState(contacts[0]); return ( <div> <ContactList contacts={contacts} selectedContact={to} onSelect={contact => setTo(contact)} /> <Chat key={to.id} contact={to} /> </div> ) } const contacts = [ { id: 0, name: 'Taylor', email: 'taylor@mail.com' }, { id: 1, name: 'Alice', email: 'alice@mail.com' }, { id: 2, name: 'Bob', email: 'bob@mail.com' } ];
Tìm hiểu sâu
Trong một ứng dụng chat thực tế, bạn có thể muốn khôi phục state input khi người dùng chọn lại người nhận trước đó. Có một vài cách để giữ state “sống” cho một component không còn hiển thị:
- Bạn có thể render tất cả các cuộc chat thay vì chỉ cuộc chat hiện tại, nhưng ẩn tất cả những cái khác với CSS. Các cuộc chat sẽ không bị xóa khỏi cây, vì vậy state cục bộ của chúng sẽ được bảo tồn. Giải pháp này hoạt động tuyệt vời cho UI đơn giản. Nhưng nó có thể trở nên rất chậm nếu các cây ẩn lớn và chứa nhiều DOM nodes.
- Bạn có thể lift state lên và giữ tin nhắn đang chờ cho mỗi người nhận trong component cha. Bằng cách này, khi các component con bị xóa, không thành vấn đề, vì chính component cha giữ thông tin quan trọng. Đây là giải pháp phổ biến nhất.
- Bạn cũng có thể sử dụng một nguồn khác ngoài React state. Ví dụ, bạn có thể muốn một bản nháp tin nhắn vẫn tồn tại ngay cả khi người dùng vô tình đóng trang. Để triển khai điều này, bạn có thể để component
Chat
khởi tạo state của nó bằng cách đọc từlocalStorage
, và lưu các bản nháp ở đó.
Bất kể bạn chọn chiến lược nào, một cuộc chat với Alice về mặt khái niệm khác với một cuộc chat với Bob, vì vậy việc trao một key
cho cây <Chat>
dựa trên người nhận hiện tại là hợp lý.
Tóm tắt
- React giữ state miễn là cùng một component được render ở cùng vị trí.
- State không được giữ trong các thẻ JSX. Nó được liên kết với vị trí cây mà bạn đặt JSX đó.
- Bạn có thể buộc một cây con đặt lại state của nó bằng cách trao cho nó một key khác.
- Đừng lồng các định nghĩa component, hoặc bạn sẽ đặt lại state một cách vô tình.
Tóm tắt
- React giữ state miễn là cùng một component được render ở cùng vị trí.
- State không được giữ trong các thẻ JSX. Nó được liên kết với vị trí cây mà bạn đặt JSX đó.
- Bạn có thể buộc một cây con đặt lại state của nó bằng cách trao cho nó một key khác.
- Đừng lồng các định nghĩa component, hoặc bạn sẽ đặt lại state một cách vô tình.
Challenge 1 of 5: Sửa text input biến mất
Ví dụ này hiển thị một tin nhắn khi bạn nhấn button. Tuy nhiên, việc nhấn button cũng vô tình đặt lại input. Tại sao điều này xảy ra? Hãy sửa nó để việc nhấn button không đặt lại text input.
import { useState } from 'react'; export default function App() { const [showHint, setShowHint] = useState(false); if (showHint) { return ( <div> <p><i>Hint: Your favorite city?</i></p> <Form /> <button onClick={() => { setShowHint(false); }}>Hide hint</button> </div> ); } return ( <div> <Form /> <button onClick={() => { setShowHint(true); }}>Show hint</button> </div> ); } function Form() { const [text, setText] = useState(''); return ( <textarea value={text} onChange={e => setText(e.target.value)} /> ); }