Khi ứng dụng của bạn phát triển, việc có kế hoạch hơn về cách tổ chức state và cách dữ liệu luân chuyển giữa các component sẽ rất hữu ích. State dư thừa hoặc trùng lặp là một nguồn gốc phổ biến của lỗi. Trong chương này, bạn sẽ học cách cấu trúc state một cách tốt, làm thế nào để giữ cho logic cập nhật state của bạn dễ bảo trì và làm thế nào để chia sẻ state giữa các component ở xa.
Trong chương này
- Cách suy nghĩ về các thay đổi UI như là các 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ẻ nó giữa các component
- Cách kiểm soát việc state được giữ lại hay đặt lại
- Cách hợp nhất logic state phức tạp vào một hàm
- Cách truyền thông tin mà không cần “truyền prop”
- Cách mở rộng quản lý state khi ứng dụng của bạn phát triển
Phản hồi lại input bằng 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 bạn muốn thấy cho các trạng thái hiển thị khác nhau của component của bạn (“trạng thái ban đầu”, “trạng thái đang nhập”, “trạng thái thành công”), và sau đó kích hoạt các thay đổi state để đáp ứng lại input 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.
Đây là một biểu mẫu trắc nghiệm được xây dựng bằng React. Lưu ý cách nó sử dụng biến state status
để xác định xem có nên kích hoạt hoặc vô hiệu hóa nút gửi hay không, và có nên hiển thị thông báo thành công hay không.
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 hồi lại Input bằng State để tìm hiểu cách tiếp cận các tương tác với tư duy hướng đến state.
Read MoreLựa chọ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ễ sửa đổi và gỡ lỗi, và một component là nguồn gốc liên tục của lỗi. 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, bạn rất dễ quên cập nhật nó và gây ra lỗi!
Ví dụ: biểu mẫu này có một 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ể xóa 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ó vẻ như là một thay đổi nhỏ, nhưng nhiều lỗi trong các ứng dụng React được sửa bằng cách này.
Ready to learn this topic?
Đọc Lựa Chọn Cấu Trúc State để tìm hiểu cách thiết kế hình dạng state để tránh lỗi.
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. Để làm điều đó, hãy xóa state khỏi cả hai, di chuyển nó đến parent chung gần nhất của chúng, và sau đó truyền nó xuống cho chúng thông qua props. Điều này được gọi là “nâng state lên”, và đó là một trong những điều phổ biến nhất bạn sẽ làm khi viết code React.
Trong ví dụ này, chỉ một panel được kích hoạt tại một thời điểm. Để đạt được điều này, thay vì giữ state hoạt động bên trong mỗi panel riêng lẻ, component parent giữ state và chỉ định các props cho các component 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 để tìm hiểu cách nâng state lên và giữ cho các component đồng bộ.
Read MoreGiữ và đặt lại state
Khi bạn render lại một component, React cần quyết định phần nào của cây giữ lại (và cập nhật), và phần nào 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. Theo mặc định, React giữ lại các phần của cây “khớp” với cây component đã render trước đó.
Tuy nhiên, đôi khi đây không phải là điều bạn muốn. Trong ứng dụng trò chuyện này, việc nhập tin nhắn và sau đó chuyển đổi người nhận không đặt lại input. Điều này có thể khiến người dùng vô tình gửi tin nhắn cho nhầm người:
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à buộc một component đặt lại 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 cho React biết rằng nếu người nhận khác, nó nên được coi là một component Chat
khác cần được tạo lại từ đầu với dữ liệu mới (và UI như input). Bây giờ, việc chuyển đổi giữa những người nhận sẽ đặt lại trường input—ngay cả khi 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 Giữ và Đặt Lại State để tìm hiểu vòng đời của state và cách kiểm soát nó.
Read MoreTrích xuất logic state vào một reducer
Các component có nhiều cập nhật state trải rộng trên nhiều trình xử lý sự kiện có thể trở nên quá tải. Đối với 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 hàm duy nhất, được gọi là “reducer”. Các trình xử lý sự kiện của bạn trở nên ngắn gọn vì chúng chỉ định các “hành động” của người dùng. Ở cuối file, hàm reducer chỉ định cách state sẽ cập nhật để đáp ứng với mỗi hành động!
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 Một Reducer để tìm hiểu cách hợp nhất logic trong hàm reducer.
Read MoreTruyền dữ liệu sâu với context
Thông thường, bạn sẽ truyền thông tin từ một component parent đến một 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 số 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 parent cung cấp một số thông tin cho bất kỳ component nào trong 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 độ của riêng nó bằng cách hỏi Section
parent và thêm một vào nó. 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 Dữ Liệu Sâu với Context để tìm hiểu về cách 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
Reducers cho phép bạn hợp nhất logic cập nhật state của một component. Context cho phép bạn truyền thông tin sâu xuống các component khác. Bạn có thể kết hợp reducers và context với nhau để quản lý state của một màn hình phức tạp.
Với phương pháp này, một component parent có state phức tạp quản lý nó bằng một reducer. Các component khác ở bất kỳ đâu sâu trong cây có thể đọc state của nó thông qua context. Họ cũng có thể dispatch các hành động để 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 để tìm hiểu cách quản lý state mở rộng trong một ứng dụng đang phát triển.
Read MoreTiếp theo là gì?
Đi tới Phản hồi lại Input bằng State để bắt đầu đọc trang chương này từng trang một!
Hoặc, nếu bạn đã quen thuộc với các chủ đề này, tại sao không đọc về Các Lối Thoát Hiểm?