Khi ứng dụng của bạn ngày càng lớn, việc có ý thức hơn về cách tổ chức state và luồng data chảy giữa các component sẽ rất hữu ích. State dư thừa hoặc trùng lặp là nguyên nhân phổ biến gây ra một loạt bug khó hiểu. Trong chương này, bạn sẽ học cách cấu trúc state một cách tốt, cách giữ cho logic cập nhật state dễ bảo trì, và cách chia sẻ state giữa các component cách xa nhau.
Trong chương này
- Cách suy nghĩ về thay đổi UI như những thay đổi state
- Cách cấu trúc state một cách tốt
- Cách “nâng state lên” để chia sẻ giữa các component
- Cách kiểm soát việc state được bảo tồn hay reset
- Cách hợp nhất logic state phức tạp trong một function
- Cách truyền thông tin mà không cần “prop drilling”
- Cách mở rộng quản lý state khi ứng dụng ngày càng lớn
Phản ứng với đầu vào thông qua state
Với React, bạn sẽ không sửa đổi UI trực tiếp từ code. Ví dụ, bạn sẽ không viết các lệnh như “vô hiệu hóa nút”, “kích hoạt nút”, “hiển thị thông báo thành công”, v.v. Thay vào đó, bạn sẽ mô tả UI mà bạn muốn thấy cho các trạng thái visual khác nhau của component (“trạng thái ban đầu”, “trạng thái đang nhập”, “trạng thái thành công”), sau đó kích hoạt những thay đổi state để phản ứng vớ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ế suy nghĩ về UI.
Đây là một form quiz được xây dựng bằng React. Lưu ý cách nó sử dụng biến state status
để xác định có nên kích hoạt hay vô hiệu hóa nút submit, và có nên hiển thị thông báo thành công thay vào đó.
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); }); }
Ready to learn this topic?
Đọc Phản Ứng Với Đầu Vào Thông Qua State để học cách tiếp cận các tương tác với mô hình tư duy điều khiển bằng state.
Read MoreChọn cấu trúc state
Cấu trúc state tốt có thể tạo ra sự khác biệt giữa một component dễ dàng sửa đổi và debug, và một component có hành vi khó đoán với một loạt bug khó hiểu. Nguyên tắc quan trọng nhất là state không nên chứa thông tin dư thừa hoặc trùng lặp. Nếu có state không cần thiết, thật dễ dàng để quên cập nhật nó và tạo ra bug!
Ví dụ, form này có biến state fullName
dư thừa:
import { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [fullName, setFullName] = useState(''); function handleFirstNameChange(e) { setFirstName(e.target.value); setFullName(e.target.value + ' ' + lastName); } function handleLastNameChange(e) { setLastName(e.target.value); setFullName(firstName + ' ' + e.target.value); } return ( <> <h2>Let’s check you in</h2> <label> First name:{' '} <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Last name:{' '} <input value={lastName} onChange={handleLastNameChange} /> </label> <p> Your ticket will be issued to: <b>{fullName}</b> </p> </> ); }
Bạn có thể loại bỏ nó và đơn giản hóa code bằng cách tính toán fullName
trong khi component đang render:
import { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const fullName = firstName + ' ' + lastName; function handleFirstNameChange(e) { setFirstName(e.target.value); } function handleLastNameChange(e) { setLastName(e.target.value); } return ( <> <h2>Let’s check you in</h2> <label> First name:{' '} <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Last name:{' '} <input value={lastName} onChange={handleLastNameChange} /> </label> <p> Your ticket will be issued to: <b>{fullName}</b> </p> </> ); }
Điều này có thể trông như một thay đổi nhỏ, nhưng một loạt bug khó hiểu trong các ứng dụng React được sửa theo cách này.
Ready to learn this topic?
Đọc Chọn Cấu Trúc State để học cách thiết kế hình dạng state để tránh bug.
Read MoreChia sẻ state giữa các component
Đôi khi, bạn muốn state của hai component luôn thay đổi cùng nhau. Để thực hiện điều này, hãy loại bỏ state khỏi cả hai, di chuyển nó lên component cha chung gần nhất, và sau đó truyền xuống cho chúng thông qua props. Điều này được gọi là “nâng state lên”, và đây là một trong những việc phổ biến nhất bạn sẽ làm khi viết code React.
Trong ví dụ này, chỉ một panel nên được kích hoạt tại một thời điểm. Để đạt được điều này, thay vì giữ state active bên trong mỗi panel riêng lẻ, component cha giữ state và chỉ định props cho các con của nó.
import { useState } from 'react'; export default function Accordion() { const [activeIndex, setActiveIndex] = useState(0); return ( <> <h2>Almaty, Kazakhstan</h2> <Panel title="About" isActive={activeIndex === 0} onShow={() => setActiveIndex(0)} > With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city. </Panel> <Panel title="Etymology" isActive={activeIndex === 1} onShow={() => setActiveIndex(1)} > The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple. </Panel> </> ); } function Panel({ title, children, isActive, onShow }) { return ( <section className="panel"> <h3>{title}</h3> {isActive ? ( <p>{children}</p> ) : ( <button onClick={onShow}> Show </button> )} </section> ); }
Ready to learn this topic?
Đọc Chia Sẻ State Giữa Các Component để học cách nâng state lên và giữ các component đồng bộ.
Read MoreBảo tồn và reset state
Khi bạn re-render một component, React cần quyết định phần nào của cây cần giữ lại (và cập nhật), và phần nào cần loại bỏ hoặc tạo lại từ đầu. Trong hầu hết các trường hợp, hành vi tự động của React hoạt động đủ tốt và dễ dự đoán. Theo mặc định, React bảo tồn các phần theo cấu trúc cây “khớp” với cây component đã được render trước đó.
Tuy nhiên, đôi khi đây không phải là điều bạn muốn. Trong ứng dụng chat này, việc nhập tin nhắn và sau đó chuyển đổi người nhận không reset input. Điều này có thể khiến người dùng vô tình gửi tin nhắn cho người sai:
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 = [ { name: 'Taylor', email: 'taylor@mail.com' }, { name: 'Alice', email: 'alice@mail.com' }, { name: 'Bob', email: 'bob@mail.com' } ];
React cho phép bạn ghi đè hành vi mặc định và bắt buộc một component reset state của nó bằng cách truyền cho nó một key
khác, như <Chat key={email} />
. Điều này báo cho React biết rằng nếu người nhận khác nhau, nó nên được coi là một component Chat
khác cần được tạo lại từ đầu với data mới (và UI như inputs). Giờ đây việc chuyển đổi giữa các người nhận sẽ reset trường input—mặc dù bạn render cùng một component.
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.email} contact={to} /> </div> ) } const contacts = [ { name: 'Taylor', email: 'taylor@mail.com' }, { name: 'Alice', email: 'alice@mail.com' }, { name: 'Bob', email: 'bob@mail.com' } ];
Ready to learn this topic?
Đọc Bảo Tồn và Reset State để học về vòng đời của state và cách kiểm soát nó.
Read MoreTrích xuất logic state vào reducer
Các component có nhiều cập nhật state được phân tán qua nhiều event handler có thể trở nên khó sử dụng. Trong những trường hợp này, bạn có thể hợp nhất tất cả logic cập nhật state bên ngoài component của bạn trong một function duy nhất, được gọi là “reducer”. Các event handler của bạn trở nên ngắn gọn vì chúng chỉ chỉ định “actions” của người dùng. Ở cuối file, function reducer chỉ định cách state nên cập nhật để phản ứng với từng action!
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; export default function TaskApp() { const [tasks, dispatch] = useReducer( tasksReducer, initialTasks ); function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId }); } return ( <> <h1>Prague itinerary</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [...tasks, { id: action.id, text: action.text, done: false }]; } case 'changed': { return tasks.map(t => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case 'deleted': { return tasks.filter(t => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'Visit Kafka Museum', done: true }, { id: 1, text: 'Watch a puppet show', done: false }, { id: 2, text: 'Lennon Wall pic', done: false } ];
Ready to learn this topic?
Đọc Trích Xuất Logic State Vào Reducer để học cách hợp nhất logic trong function reducer.
Read MoreTruyền data sâu với context
Thông thường, bạn sẽ truyền thông tin từ component cha đến component con thông qua props. Nhưng việc truyền props có thể trở nên bất tiện nếu bạn cần truyền một prop qua nhiều component, hoặc nếu nhiều component cần cùng một thông tin. Context cho phép component cha làm cho một số thông tin có sẵn cho bất kỳ component nào theo cấu trúc cây bên dưới nó—bất kể nó sâu đến đâu—mà không cần truyền nó một cách rõ ràng thông qua props.
Ở đây, component Heading
xác định cấp độ heading của nó bằng cách “hỏi” Section
gần nhất về cấp độ của nó. Mỗi Section
theo dõi cấp độ riêng của nó bằng cách hỏi Section
cha và cộng thêm một. Mọi Section
cung cấp thông tin cho tất cả các component bên dưới nó mà không cần truyền props—nó thực hiện điều đó thông qua context.
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading>Title</Heading> <Section> <Heading>Heading</Heading> <Heading>Heading</Heading> <Heading>Heading</Heading> <Section> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Section> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> </Section> </Section> </Section> </Section> ); }
Ready to learn this topic?
Đọc Truyền Data Sâu Với Context để học về việc sử dụng context như một giải pháp thay thế cho việc truyền props.
Read MoreMở rộng với reducer và context
Reducer cho phép bạn tổng hợp logic cập nhật state của component. Context cho phép bạn truyền thông tin xuống sâu cho các component khác. Bạn có thể kết hợp reducer và context cùng nhau để quản lý state của một màn hình phức tạp.
Với cách tiếp cận này, một component cha có state phức tạp sẽ quản lý nó bằng reducer. Các component khác ở bất kỳ vị trí sâu nào theo cấu trúc cây có thể đọc state của nó thông qua context. Chúng cũng có thể dispatch các action để cập nhật state đó.
import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import { TasksProvider } from './TasksContext.js'; export default function TaskApp() { return ( <TasksProvider> <h1>Day off in Kyoto</h1> <AddTask /> <TaskList /> </TasksProvider> ); }
Ready to learn this topic?
Đọc Mở Rộng Với Reducer và Context để học cách quản lý state mở rộng trong một ứng dụng ngày càng lớn.
Read MoreTiếp theo là gì?
Hãy chuyển đến Phản Ứng Với Đầu Vào Thông Qua State để bắt đầu đọc chương này từng trang!
Hoặc, nếu bạn đã quen thuộc với những chủ đề này, tại sao không đọc về Escape Hatches?