Phản hồi Đầu vào bằng State
React cung cấp một cách khai báo để thao tác UI. Thay vì thao tác trực tiếp các phần tử UI riêng lẻ, bạn mô tả các trạng thái khác nhau mà component của bạn có thể ở và chuyển đổi giữa chúng để phản hồi đầu vào của người dùng. Điều này tương tự như cách các nhà thiết kế nghĩ về UI.
Bạn sẽ được học
- Sự khác biệt giữa lập trình UI khai báo và lập trình UI mệnh lệnh
- Cách liệt kê các trạng thái hiển thị khác nhau mà component của bạn có thể ở
- Cách kích hoạt các thay đổi giữa các trạng thái hiển thị khác nhau từ code
So sánh UI khai báo với UI mệnh lệnh
Khi bạn thiết kế các tương tác UI, bạn có thể nghĩ về cách UI thay đổi để phản hồi các hành động của người dùng. Hãy xem xét một biểu mẫu cho phép người dùng gửi câu trả lời:
- Khi bạn nhập nội dung gì đó vào biểu mẫu, nút “Gửi” sẽ được bật.
- Khi bạn nhấn “Gửi”, cả biểu mẫu và nút sẽ bị tắt, và một spinner xuất hiện.
- Nếu yêu cầu mạng thành công, biểu mẫu sẽ bị ẩn, và thông báo “Cảm ơn” xuất hiện.
- Nếu yêu cầu mạng không thành công, một thông báo lỗi xuất hiện, và biểu mẫu sẽ được bật lại.
Trong lập trình mệnh lệnh, điều trên tương ứng trực tiếp với cách bạn triển khai tương tác. Bạn phải viết các hướng dẫn chính xác để thao tác UI tùy thuộc vào những gì vừa xảy ra. Đây là một cách khác để nghĩ về điều này: hãy tưởng tượng bạn đang đi cạnh ai đó trong xe hơi và chỉ cho họ từng ngã rẽ nơi cần đi.

Illustrated by Rachel Lee Nabors
Họ không biết bạn muốn đi đâu, họ chỉ làm theo lệnh của bạn. (Và nếu bạn chỉ sai đường, bạn sẽ đến nhầm chỗ!) Nó được gọi là mệnh lệnh vì bạn phải “ra lệnh” cho từng phần tử, từ spinner đến nút, cho máy tính biết cách cập nhật UI.
Trong ví dụ về lập trình UI mệnh lệnh này, biểu mẫu được xây dựng không có React. Nó chỉ sử dụng DOM của trình duyệt:
async function handleFormSubmit(e) { e.preventDefault(); disable(textarea); disable(button); show(loadingMessage); hide(errorMessage); try { await submitForm(textarea.value); show(successMessage); hide(form); } catch (err) { show(errorMessage); errorMessage.textContent = err.message; } finally { hide(loadingMessage); enable(textarea); enable(button); } } function handleTextareaChange() { if (textarea.value.length === 0) { disable(button); } else { enable(button); } } function hide(el) { el.style.display = 'none'; } function show(el) { el.style.display = ''; } function enable(el) { el.disabled = false; } function disable(el) { el.disabled = true; } function submitForm(answer) { // Pretend it's hitting the network. return new Promise((resolve, reject) => { setTimeout(() => { if (answer.toLowerCase() === 'istanbul') { resolve(); } else { reject(new Error('Good guess but a wrong answer. Try again!')); } }, 1500); }); } let form = document.getElementById('form'); let textarea = document.getElementById('textarea'); let button = document.getElementById('button'); let loadingMessage = document.getElementById('loading'); let errorMessage = document.getElementById('error'); let successMessage = document.getElementById('success'); form.onsubmit = handleFormSubmit; textarea.oninput = handleTextareaChange;
Thao tác UI một cách mệnh lệnh hoạt động đủ tốt cho các ví dụ riêng lẻ, nhưng nó trở nên khó quản lý hơn theo cấp số nhân trong các hệ thống phức tạp hơn. Hãy tưởng tượng việc cập nhật một trang đầy các biểu mẫu khác nhau như thế này. Việc thêm một phần tử UI mới hoặc một tương tác mới sẽ yêu cầu kiểm tra cẩn thận tất cả code hiện có để đảm bảo bạn không gây ra lỗi (ví dụ: quên hiển thị hoặc ẩn nội dung gì đó).
React được xây dựng để giải quyết vấn đề này.
Trong React, bạn không thao tác trực tiếp UI—nghĩa là bạn không bật, tắt, hiển thị hoặc ẩn các component trực tiếp. Thay vào đó, bạn khai báo những gì bạn muốn hiển thị, và React sẽ tìm ra cách cập nhật UI. Hãy nghĩ đến việc bắt taxi và nói với tài xế nơi bạn muốn đến thay vì chỉ cho họ chính xác nơi cần rẽ. Công việc của tài xế là đưa bạn đến đó, và họ thậm chí có thể biết một số đường tắt mà bạn chưa xem xét!

Illustrated by Rachel Lee Nabors
Tư duy về UI một cách khai báo
Bạn đã thấy cách triển khai một biểu mẫu một cách mệnh lệnh ở trên. Để hiểu rõ hơn về cách tư duy trong React, bạn sẽ thực hiện lại UI này trong React bên dưới:
- Xác định các trạng thái hiển thị khác nhau của component của bạn
- Xác định điều gì kích hoạt những thay đổi trạng thái đó
- Biểu diễn trạng thái trong bộ nhớ bằng
useState
- Loại bỏ bất kỳ biến trạng thái không cần thiết nào
- Kết nối các trình xử lý sự kiện để đặt trạng thái
Bước 1: Xác định các trạng thái hiển thị khác nhau của component của bạn
Trong khoa học máy tính, bạn có thể nghe nói về một “máy trạng thái” đang ở một trong số các “trạng thái”. Nếu bạn làm việc với một nhà thiết kế, bạn có thể đã thấy các bản mô phỏng cho các “trạng thái hiển thị” khác nhau. React đứng ở giao điểm giữa thiết kế và khoa học máy tính, vì vậy cả hai ý tưởng này đều là nguồn cảm hứng.
Đầu tiên, bạn cần hình dung tất cả các “trạng thái” khác nhau của UI mà người dùng có thể thấy:
- Trống: Biểu mẫu có nút “Gửi” bị tắt.
- Đang nhập: Biểu mẫu có nút “Gửi” được bật.
- Đang gửi: Biểu mẫu hoàn toàn bị tắt. Spinner được hiển thị.
- Thành công: Thông báo “Cảm ơn” được hiển thị thay vì biểu mẫu.
- Lỗi: Giống như trạng thái Đang nhập, nhưng có thêm thông báo lỗi.
Giống như một nhà thiết kế, bạn sẽ muốn “mô phỏng” hoặc tạo “bản mô phỏng” cho các trạng thái khác nhau trước khi bạn thêm logic. Ví dụ: đây là bản mô phỏng chỉ cho phần hiển thị của biểu mẫu. Bản mô phỏng này được điều khiển bởi một prop có tên là status
với giá trị mặc định là 'empty'
:
export default function Form({ status = 'empty' }) { if (status === 'success') { return <h1>That's right!</h1> } return ( <> <h2>City quiz</h2> <p> In which city is there a billboard that turns air into drinkable water? </p> <form> <textarea /> <br /> <button> Submit </button> </form> </> ) }
Bạn có thể gọi prop đó là bất cứ điều gì bạn thích, việc đặt tên không quan trọng. Hãy thử chỉnh sửa status = 'empty'
thành status = 'success'
để thấy thông báo thành công xuất hiện. Mô phỏng cho phép bạn nhanh chóng lặp lại trên UI trước khi bạn kết nối bất kỳ logic nào. Dưới đây là một nguyên mẫu đầy đủ hơn của cùng một component, vẫn “được điều khiển” bởi prop status
:
export default function Form({ // Try 'submitting', 'error', 'success': status = 'empty' }) { if (status === 'success') { return <h1>That's right!</h1> } return ( <> <h2>City quiz</h2> <p> In which city is there a billboard that turns air into drinkable water? </p> <form> <textarea disabled={ status === 'submitting' } /> <br /> <button disabled={ status === 'empty' || status === 'submitting' }> Submit </button> {status === 'error' && <p className="Error"> Good guess but a wrong answer. Try again! </p> } </form> </> ); }
Tìm hiểu sâu
Nếu một component có nhiều trạng thái hiển thị, có thể thuận tiện để hiển thị tất cả chúng trên một trang:
import Form from './Form.js'; let statuses = [ 'empty', 'typing', 'submitting', 'success', 'error', ]; export default function App() { return ( <> {statuses.map(status => ( <section key={status}> <h4>Form ({status}):</h4> <Form status={status} /> </section> ))} </> ); }
Các trang như thế này thường được gọi là “living styleguides” hoặc “storybooks”.
Bước 2: Xác định điều gì kích hoạt những thay đổi trạng thái đó
Bạn có thể kích hoạt cập nhật trạng thái để phản hồi hai loại đầu vào:
- Đầu vào của con người, như nhấp vào nút, nhập vào một trường, điều hướng một liên kết.
- Đầu vào của máy tính, như phản hồi mạng đến, thời gian chờ hoàn thành, hình ảnh tải.


Illustrated by Rachel Lee Nabors
Trong cả hai trường hợp, bạn phải đặt biến trạng thái để cập nhật UI. Đối với biểu mẫu bạn đang phát triển, bạn sẽ cần thay đổi trạng thái để phản hồi một vài đầu vào khác nhau:
- Thay đổi đầu vào văn bản (con người) sẽ chuyển nó từ trạng thái Trống sang trạng thái Đang nhập hoặc ngược lại, tùy thuộc vào việc hộp văn bản có trống hay không.
- Nhấp vào nút Gửi (con người) sẽ chuyển nó sang trạng thái Đang gửi.
- Phản hồi mạng thành công (máy tính) sẽ chuyển nó sang trạng thái Thành công.
- Phản hồi mạng không thành công (máy tính) sẽ chuyển nó sang trạng thái Lỗi với thông báo lỗi phù hợp.
Để giúp hình dung luồng này, hãy thử vẽ từng trạng thái trên giấy dưới dạng một vòng tròn được gắn nhãn và mỗi thay đổi giữa hai trạng thái dưới dạng một mũi tên. Bạn có thể phác thảo nhiều luồng theo cách này và sắp xếp các lỗi trước khi triển khai.


Trạng thái biểu mẫu
Bước 3: Biểu diễn trạng thái trong bộ nhớ bằng useState
Tiếp theo, bạn sẽ cần biểu diễn các trạng thái hiển thị của component của bạn trong bộ nhớ bằng useState
. Sự đơn giản là chìa khóa: mỗi phần của trạng thái là một “mảnh ghép chuyển động” và bạn muốn càng ít “mảnh ghép chuyển động” càng tốt. Càng phức tạp thì càng có nhiều lỗi!
Bắt đầu với trạng thái tuyệt đối phải có ở đó. Ví dụ: bạn sẽ cần lưu trữ answer
cho đầu vào và error
(nếu có) để lưu trữ lỗi cuối cùng:
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
Sau đó, bạn sẽ cần một biến trạng thái đại diện cho một trong các trạng thái hiển thị mà bạn muốn hiển thị. Thường có nhiều hơn một cách để biểu diễn điều đó trong bộ nhớ, vì vậy bạn sẽ cần thử nghiệm với nó.
Nếu bạn gặp khó khăn trong việc nghĩ ra cách tốt nhất ngay lập tức, hãy bắt đầu bằng cách thêm đủ trạng thái mà bạn chắc chắn rằng tất cả các trạng thái hiển thị có thể có đều được bao phủ:
const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);
Ý tưởng đầu tiên của bạn có thể không phải là tốt nhất, nhưng điều đó không sao—tái cấu trúc trạng thái là một phần của quy trình!
Bước 4: Loại bỏ bất kỳ biến trạng thái không cần thiết nào
Bạn muốn tránh trùng lặp trong nội dung trạng thái để bạn chỉ theo dõi những gì cần thiết. Dành một chút thời gian để tái cấu trúc cấu trúc trạng thái của bạn sẽ giúp các component của bạn dễ hiểu hơn, giảm trùng lặp và tránh các ý nghĩa không mong muốn. Mục tiêu của bạn là ngăn chặn các trường hợp trạng thái trong bộ nhớ không đại diện cho bất kỳ UI hợp lệ nào mà bạn muốn người dùng thấy. (Ví dụ: bạn không bao giờ muốn hiển thị thông báo lỗi và tắt đầu vào cùng một lúc, nếu không người dùng sẽ không thể sửa lỗi!)
Dưới đây là một số câu hỏi bạn có thể hỏi về các biến trạng thái của mình:
- Trạng thái này có gây ra nghịch lý không? Ví dụ:
isTyping
vàisSubmitting
không thể đồng thời làtrue
. Một nghịch lý thường có nghĩa là trạng thái không đủ ràng buộc. Có bốn tổ hợp có thể có của hai boolean, nhưng chỉ ba tổ hợp tương ứng với các trạng thái hợp lệ. Để loại bỏ trạng thái “không thể”, bạn có thể kết hợp chúng thành mộtstatus
phải là một trong ba giá trị:'typing'
,'submitting'
hoặc'success'
. - Thông tin tương tự đã có trong một biến trạng thái khác chưa? Một nghịch lý khác:
isEmpty
vàisTyping
không thể đồng thời làtrue
. Bằng cách tạo chúng thành các biến trạng thái riêng biệt, bạn có nguy cơ chúng không đồng bộ và gây ra lỗi. May mắn thay, bạn có thể loại bỏisEmpty
và thay vào đó kiểm traanswer.length === 0
. - Bạn có thể nhận được thông tin tương tự từ nghịch đảo của một biến trạng thái khác không? Không cần
isError
vì bạn có thể kiểm traerror !== null
thay thế.
Sau khi dọn dẹp này, bạn còn lại 3 (giảm từ 7!) biến trạng thái cần thiết:
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'
Bạn biết chúng là cần thiết, vì bạn không thể loại bỏ bất kỳ biến nào trong số chúng mà không làm hỏng chức năng.
Tìm hiểu sâu
Ba biến này là một biểu diễn đủ tốt về trạng thái của biểu mẫu này. Tuy nhiên, vẫn còn một số trạng thái trung gian không hoàn toàn có ý nghĩa. Ví dụ: error
khác null không có ý nghĩa khi status
là 'success'
. Để mô hình hóa trạng thái chính xác hơn, bạn có thể trích xuất nó vào một reducer. Reducer cho phép bạn hợp nhất nhiều biến trạng thái thành một đối tượng duy nhất và hợp nhất tất cả logic liên quan!
Bước 5: Kết nối các trình xử lý sự kiện để đặt trạng thái
Cuối cùng, tạo các trình xử lý sự kiện để cập nhật trạng thái. Dưới đây là biểu mẫu cuối cùng, với tất cả các trình xử lý sự kiện được kết nối:
import { useState } from 'react'; export default function Form() { const [answer, setAnswer] = useState(''); const [error, setError] = useState(null); const [status, setStatus] = useState('typing'); if (status === 'success') { return <h1>That's right!</h1> } async function handleSubmit(e) { e.preventDefault(); setStatus('submitting'); try { await submitForm(answer); setStatus('success'); } catch (err) { setStatus('typing'); setError(err); } } function handleTextareaChange(e) { setAnswer(e.target.value); } return ( <> <h2>City quiz</h2> <p> In which city is there a billboard that turns air into drinkable water? </p> <form onSubmit={handleSubmit}> <textarea value={answer} onChange={handleTextareaChange} disabled={status === 'submitting'} /> <br /> <button disabled={ answer.length === 0 || status === 'submitting' }> Submit </button> {error !== null && <p className="Error"> {error.message} </p> } </form> </> ); } function submitForm(answer) { // Pretend it's hitting the network. return new Promise((resolve, reject) => { setTimeout(() => { let shouldError = answer.toLowerCase() !== 'lima' if (shouldError) { reject(new Error('Good guess but a wrong answer. Try again!')); } else { resolve(); } }, 1500); }); }
Mặc dù code này dài hơn ví dụ mệnh lệnh ban đầu, nhưng nó ít bị lỗi hơn nhiều. Việc thể hiện tất cả các tương tác như là các thay đổi trạng thái cho phép bạn giới thiệu các trạng thái hiển thị mới mà không làm hỏng các trạng thái hiện có. Nó cũng cho phép bạn thay đổi những gì sẽ được hiển thị trong mỗi trạng thái mà không thay đổi logic của chính tương tác đó.
Tóm tắt
- Lập trình khai báo có nghĩa là mô tả giao diện người dùng cho mỗi trạng thái hiển thị thay vì quản lý chi tiết giao diện người dùng (mệnh lệnh).
- Khi phát triển một component:
- Xác định tất cả các trạng thái hiển thị của nó.
- Xác định các tác nhân kích hoạt thay đổi trạng thái từ con người và máy tính.
- Mô hình hóa trạng thái bằng
useState
. - Loại bỏ trạng thái không cần thiết để tránh lỗi và nghịch lý.
- Kết nối các trình xử lý sự kiện để đặt trạng thái.
Challenge 1 of 3: Thêm và xóa một lớp CSS
Làm cho việc nhấp vào hình ảnh xóa lớp CSS background--active
khỏi <div>
bên ngoài, nhưng thêm lớp picture--active
vào <img>
. Nhấp lại vào nền sẽ khôi phục các lớp CSS ban đầu.
Về mặt hình ảnh, bạn nên mong đợi rằng việc nhấp vào hình ảnh sẽ loại bỏ nền màu tím và làm nổi bật đường viền của hình ảnh. Nhấp vào bên ngoài hình ảnh sẽ làm nổi bật nền, nhưng loại bỏ điểm nổi bật của đường viền hình ảnh.
export default function Picture() { return ( <div className="background background--active"> <img className="picture" alt="Rainbow houses in Kampung Pelangi, Indonesia" src="https://i.imgur.com/5qwVYb1.jpeg" /> </div> ); }