Lựa chọn cấu trúc cho state
Cấu trúc state tốt có thể tạo ra sự khác biệt giữa một component dễ chỉnh sửa và debug và một component bị lỗi liên tục. Sau đây là một số mẹo bạn nên cân nhắc khi cấu trúc state.
Bạn sẽ được học
- Khi nào nên sử dụng nhiều state hay một state duy nhất cho nhiều giá trị
- Những điều cần tránh khi tổ chức state
- Cách để fix những lỗi phổ biến khi cấu trúc state
Nguyên tắc khi cấu trúc state
Khi bạn viết một component có chứa một vài state, bạn sẽ phải đưa ra quyết định về việc có bao nhiêu state cần sử dụng và cấu trúccủa chúng. Mặc dù có thể viết chương trình đúng ngay cả khi cấu trúc state không tối ưu, nhưng có một vài nguyên tắc có thể giúp bạn đưa ra những lựa chọn tốt hơn:
- Nhóm các state có liên quan. Nếu bạn luôn phải cập nhật hai hoặc nhiều hơn state cùng một lúc, hãy nghĩ đến việc gộp chúng vào một state duy nhất.
- Tránh sự mâu thuẫn trong state. Khi state được cấu trúc sao cho một số phần của state có thể mâu thuẫn và “không đồng ý” với nhau, bạn để lại cơ hội cho lỗi. Hãy cố gắng tránh điều này.
- Tránh dư thừa state. Nếu bạn có thể tính toán một số thông tin từ props của component hoặc các state hiện tại của nó trong quá trình render, bạn không nên đặt thông tin đó vào state của component đó.
- Tránh trùng lặp trong state. Khi cùng một data được lặp lại giữa nhiều state hoặc trong các object lồng nhau, rất khó để giữ cho chúng đồng bộ với nhau. Hạn chế sự trùng lặp này khi bạn có thể.
- Tránh lồng state quá sâu. State có cấu trúc phân cấp sâu rất không thuận tiện để cập nhật. Khi có thể, hãy ưu tiên cấu trúc state theo cách phẳng.
Mục tiêu đằng sau các quy tắc này là làm cho state dễ dàng cập nhật mà không gây ra lỗi. Xoá data dư thừa và trùng lặp khỏi state giúp đảm bảo rằng tất cả các phần của nó luông đồng bộ. Điều này gần giống với cách một database engineer muốn “chuẩn hoá” cấu trúc database để giảm khả năng xảy ra lỗi. Để dùng lời của Albert Einstein, “Hãy làm cho state của bạn đơn giản nhất có thể—nhưng không đơn giản hơn.”
Giờ hãy xem cách các nguyên tắc này được áp dụng trong thực tế.
Nhóm các state liên quan
Đôi khi bạn có thể không chắc chắn giữa việc sử dụng nhiều state hay một state duy nhất cho nhiều giá trị.
Bạn nên làm như thế này?
const [x, setX] = useState(0);
const [y, setY] = useState(0);
Hay như thế này?
const [position, setPosition] = useState({ x: 0, y: 0 });
Về mặt kỹ thuật, bạn có thể sử dụng một trong hai cách trên. Nhưng nếu hai state luôn thay đổi cùng nhau, việc gộp chúng lại với nhau có thể là một ý tưởng tốt. Khi đó, bạn không cần phải lo lắng về việc giữ cho chúng đồng bộ, giống như trong ví dụ dưới đây khi di chuyển con trỏ sẽ cập nhật cả hai tọa độ của chấm đỏ:
import { useState } from 'react'; export default function MovingDot() { const [position, setPosition] = useState({ x: 0, y: 0 }); return ( <div onPointerMove={e => { setPosition({ x: e.clientX, y: e.clientY }); }} style={{ position: 'relative', width: '100vw', height: '100vh', }}> <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ) }
Một trường hợp khác là bạn sẽ nhóm data vào một object hoặc một mảng khi bạn không biết bạn sẽ cần bao nhiêu state. Ví dụ, nó rất hữu ích khi bạn có một form mà người dùng có thể thêm các trường tùy chỉnh.
Tránh mâu thuẫn trong state
Đây là một form phản hồi của khách sạn với state isSending
và isSent
:
import { useState } from 'react'; export default function FeedbackForm() { const [text, setText] = useState(''); const [isSending, setIsSending] = useState(false); const [isSent, setIsSent] = useState(false); async function handleSubmit(e) { e.preventDefault(); setIsSending(true); await sendMessage(text); setIsSending(false); setIsSent(true); } if (isSent) { return <h1>Cảm ơn bạn đã phản hồi!</h1> } return ( <form onSubmit={handleSubmit}> <p></p> <p>Bạn thấy kỳ nghỉ của mình tại The Prancing Pony thế nào?</p> <textarea disabled={isSending} value={text} onChange={e => setText(e.target.value)} /> <br /> <button disabled={isSending} type="submit" > Gửi </button> {isSending && <p>Đang gửi...</p>} </form> ); } // Pretend to send a message. function sendMessage(text) { return new Promise(resolve => { setTimeout(resolve, 2000); }); }
Trong khi đoạn code này hoạt động, nó để lại cơ hội cho các trạng thái “không thể xảy ra” xảy ra. Ví dụ, nếu bạn quên gọi setIsSent
và setIsSending
cùng một lúc, bạn có thể kết thúc trong tình huống mà cả isSending
và isSent
đều là true
cùng một lúc. Component của bạn càng phức tạp, việc hiểu xem đã xảy ra điều gì càng khó khăn.
Vì isSending
và isSent
không nên cùng true
trong bất kỳ trường hợp nào, tốt hơn là nó nên được thay thế bởi một state được gọi là status
và nó có thể mang một trong các giá trị sau: 'typing'
(ban đầu), 'sending'
, và 'sent'
:
import { useState } from 'react'; export default function FeedbackForm() { const [text, setText] = useState(''); const [status, setStatus] = useState('typing'); async function handleSubmit(e) { e.preventDefault(); setStatus('sending'); await sendMessage(text); setStatus('sent'); } const isSending = status === 'sending'; const isSent = status === 'sent'; if (isSent) { return <h1>Cảm ơn bạn đã phản hồi!</h1> } return ( <form onSubmit={handleSubmit}> <p>Bạn thấy kỳ nghỉ của mình tại The Prancing Pony như thế nào?</p> <textarea disabled={isSending} value={text} onChange={e => setText(e.target.value)} /> <br /> <button disabled={isSending} type="submit" > Gửi </button> {isSending && <p>Đang gửi...</p>} </form> ); } // Pretend to send a message. function sendMessage(text) { return new Promise(resolve => { setTimeout(resolve, 2000); }); }
Bạn cũng có thể khai báo thêm một số hằng số để dễ đọc:
const isSending = status === 'sending';
const isSent = status === 'sent';
Vì chúng không phải là state, nên bạn không cần phải lo lắng về việc chúng không đồng bộ với nhau.
Tránh dư thừa state
Nếu bạn có thể tính toán một số thông tin từ props của component hoặc các state hiện tại của nó trong quá trình render, bạn không nên đặt thông tin đó vào state của component đó.
Ví dụ, hãy xem form này. Nó hoạt động, nhưng bạn có thể tìm thấy bất kỳ state nào dư thừa không?
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(lastName + ' ' + e.target.value); } function handleLastNameChange(e) { setLastName(e.target.value); setFullName(e.target.value + ' ' + firstName); } return ( <> <h2>Đăng ký thông tin</h2> <label> Họ:{' '} <input value={lastName} onChange={handleLastNameChange} /> </label> <label> Tên:{' '} <input value={firstName} onChange={handleFirstNameChange} /> </label> <p> Vé của bạn sẽ được cấp cho: <b>{fullName}</b> </p> </> ); }
Form này có chứa ba state: firstName
, lastName
và fullName
. Tuy nhiên, fullName
là dư thừa. Bạn luôn có thể tính được fullName
từ firstName
và lastName
khi render, do đó hãy xoá nó khỏi state.
Đây là cách bạn có thể làm điều đó:
import { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const fullName = lastName + ' ' + firstName; function handleFirstNameChange(e) { setFirstName(e.target.value); } function handleLastNameChange(e) { setLastName(e.target.value); } return ( <> <h2>Đăng ký thông tin</h2> <label> Họ:{' '} <input value={lastName} onChange={handleLastNameChange} /> </label> <label> Tên:{' '} <input value={firstName} onChange={handleFirstNameChange} /> </label> <p> Vé của bạn sẽ được cấp cho: <b>{fullName}</b> </p> </> ); }
Giờ đây, fullName
không là state. Thay vào đó, nó được tính toán trong quá trình render:
const fullName = lastName + ' ' + firstName;
As a result, the change handlers don’t need to do anything special to update it. When you call setFirstName
or setLastName
, you trigger a re-render, and then the next fullName
will be calculated from the fresh data.
Do đó, các handler không cần phải làm bất cứ điều gì đặc biệt để cập nhật nó. Khi bạn gọi setFirstName
hoặc setLastName
, bạn kích hoạt một lần re-render, và sau đó fullName
sẽ được tính toán từ dữ liệu mới.
Tìm hiểu sâu
Một ví dụ cho sự dư thừa state phổ biến là đoạn code như sau:
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);
Ở đây, color
mang giá trị khỏi tạo là prop messageColor
. Vấn đề là nếu component cha truyền một giá trị khác của messageColor
sau này (ví dụ, 'red'
thay vì 'blue'
), biến color
state variable sẽ không được cập nhật! State chỉ được khởi tạo trong lần render đầu tiên.
Đây là lý do tại sao sao chép một số prop vào một state có thể dẫn đến sự nhầm lẫn. Thay vào đó, hãy sử dụng prop messageColor
trực tiếp trong code của bạn. Nếu bạn muốn đặt tên ngắn gọn hơn, hãy gán cho nó một hằng số:
function Message({ messageColor }) {
const color = messageColor;
Bằng cách này, nó sẽ đồng bộ với prop được truyền từ component cha.
Sao chép props vào state chỉ hợp lý khi bạn muốn bỏ qua tất cả các cập nhật cho một prop cụ thể. Theo quy ước, bắt đầu tên prop với initial
hoặc default
để làm rõ rằng các giá trị mới của nó bị bỏ qua:
function Message({ initialColor }) {
// State `color` mang giá trị *đầu tiên* của `initialColor`.
// Các thay đổi sau này của prop `initialColor` sẽ bị bỏ qua.
const [color, setColor] = useState(initialColor);
Tránh trùng lặp trong state
Component Menu này cho phép bạn chọn một món ăn từ danh sách và hiển thị món ăn đã chọn:
import { useState } from 'react'; const initialItems = [ { title: 'phở', id: 0 }, { title: 'bún chả', id: 1 }, { title: 'bánh mì', id: 2 }, ]; export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedItem, setSelectedItem] = useState( items[0] ); return ( <> <h2>Bạn muốn dùng món gì</h2> <ul> {items.map(item => ( <li key={item.id}> {item.title} {' '} <button onClick={() => { setSelectedItem(item); }}>Chọn</button> </li> ))} </ul> <p>Bạn đã chọn {selectedItem.title}.</p> </> ); }
Hiện tại, nó lưu món ăn được chọn dưới dạng một object trong state selectedItem
. Tuy nhiên, điều này là không tốt: nội dung của selectedItem
giống một object trong danh sách items
. Điều này có nghĩa là thông tin về món ăn đó được lặp lại ở hai nơi.
Tại sao điều này là một vấn đề? Hãy thử cho phép người dùng chỉnh sửa món ăn trong danh sách:
import { useState } from 'react'; const initialItems = [ { title: 'phở', id: 0 }, { title: 'bún chả', id: 1 }, { title: 'bánh mì', id: 2 }, ]; export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedItem, setSelectedItem] = useState( items[0] ); function handleItemChange(id, e) { setItems(items.map(item => { if (item.id === id) { return { ...item, title: e.target.value, }; } else { return item; } })); } return ( <> <h2>Bạn muốn dùng món gì?</h2> <ul> {items.map((item, index) => ( <li key={item.id}> <input value={item.title} onChange={e => { handleItemChange(item.id, e) }} /> {' '} <button onClick={() => { setSelectedItem(item); }}>Chọn</button> </li> ))} </ul> <p>Bạn đã chọn {selectedItem.title}.</p> </> ); }
Hãy để ý là khi bạn nhấn “Chọn” một món ăn sau đó chỉnh sửa món đó, ô input được cập nhật nhưng nhãn ở dưới không phản ánh những chỉnh sửa. Điều này xảy ra vì bạn đã trùng lặp state, và bạn đã quên cập nhật selectedItem
.
Mặc dù bạn cũng có thể cập nhật selectedItem
, một cách fix dễ hơn là xoá bỏ sự trùng lặp. Trong ví dụ này, thay vì một object selectedItem
(tạo ra sự trùng lặp với các object trong items
), bạn giữ selectedId
trong state, và sau đó lấy selectedItem
bằng cách tìm kiếm mảng items
để tìm một item với ID đó:
import { useState } from 'react'; const initialItems = [ { title: 'phở', id: 0 }, { title: 'bún chả', id: 1 }, { title: 'bánh mì', id: 2 }, ]; export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedId, setSelectedId] = useState(0); const selectedItem = items.find(item => item.id === selectedId ); function handleItemChange(id, e) { setItems(items.map(item => { if (item.id === id) { return { ...item, title: e.target.value, }; } else { return item; } })); } return ( <> <h2>Bạn muốn dùng món gì?</h2> <ul> {items.map((item, index) => ( <li key={item.id}> <input value={item.title} onChange={e => { handleItemChange(item.id, e) }} /> {' '} <button onClick={() => { setSelectedId(item.id); }}>Chọn</button> </li> ))} </ul> <p>Bạn đã chọn {selectedItem.title}.</p> </> ); }
State được sử dụng trước đây trông như thế này:
items = [{ id: 0, title: 'phở' }, ...]
selectedItem = { id: 0, title: 'phở' }
Nhưng sau khi thay đổi, nó trông như thế này:
items = [{ id: 0, title: 'phở'}, ...]
selectedId = 0
Sự trùng lặp đã biến mất, và bạn chỉ giữ lại state cần thiết!
Giờ nếu bạn chỉnh sửa món ăn đã chọn, nội dung tin nhắn bên dưới sẽ cập nhật ngay lập tức. Điều này xảy ra vì setItems
kích hoạt một lần re-render, và items.find(...)
sẽ tìm thấy món ăn với tiêu đề đã cập nhật. Bạn không cần giữ lại món đã chọn trong state, vì chỉ ID đã chọn mới là cần thiết. Phần còn lại có thể được tính toán trong quá trình render.
Tránh sử dụng state lồng nhau quá sâu
Hãy tưởng tượng một kế hoạch du lịch bao gồm các hành tinh, châu lục và quốc gia. Bạn có thể muốn cấu trúc state của nó bằng cách sử dụng các object và mảng lồng nhau, như trong ví dụ này:
export const initialTravelPlan = { id: 0, title: '(Root)', childPlaces: [{ id: 1, title: 'Trái Đất', childPlaces: [{ id: 2, title: 'Châu Phi', childPlaces: [{ id: 3, title: 'Botswana', childPlaces: [] }, { id: 4, title: 'Egypt', childPlaces: [] }, { id: 5, title: 'Kenya', childPlaces: [] }, { id: 6, title: 'Madagascar', childPlaces: [] }, { id: 7, title: 'Morocco', childPlaces: [] }, { id: 8, title: 'Nigeria', childPlaces: [] }, { id: 9, title: 'Nam Phi', childPlaces: [] }] }, { id: 10, title: 'Châu Mỹ', childPlaces: [{ id: 11, title: 'Argentina', childPlaces: [] }, { id: 12, title: 'Brazil', childPlaces: [] }, { id: 13, title: 'Barbados', childPlaces: [] }, { id: 14, title: 'Canada', childPlaces: [] }, { id: 15, title: 'Jamaica', childPlaces: [] }, { id: 16, title: 'Mexico', childPlaces: [] }, { id: 17, title: 'Trinidad and Tobago', childPlaces: [] }, { id: 18, title: 'Venezuela', childPlaces: [] }] }, { id: 19, title: 'Châu Á', childPlaces: [{ id: 20, title: 'Trung Quốc', childPlaces: [] }, { id: 21, title: 'Ấn Độ', childPlaces: [] }, { id: 22, title: 'Singapore', childPlaces: [] }, { id: 23, title: 'Hàn Quốc', childPlaces: [] }, { id: 24, title: 'Thái Lan', childPlaces: [] }, { id: 25, title: 'Việt Nam', childPlaces: [] }] }, { id: 26, title: 'Châu Âu', childPlaces: [{ id: 27, title: 'Croatia', childPlaces: [], }, { id: 28, title: 'Pháp', childPlaces: [], }, { id: 29, title: 'Đức', childPlaces: [], }, { id: 30, title: 'Italy', childPlaces: [], }, { id: 31, title: 'Portugal', childPlaces: [], }, { id: 32, title: 'Spain', childPlaces: [], }, { id: 33, title: 'Thổ Nhĩ Kỳ', childPlaces: [], }] }, { id: 34, title: 'Châu Đại Dương', childPlaces: [{ id: 35, title: 'Úc', childPlaces: [], }, { id: 36, title: 'Bora Bora (French Polynesia)', childPlaces: [], }, { id: 37, title: 'Easter Island (Chile)', childPlaces: [], }, { id: 38, title: 'Fiji', childPlaces: [], }, { id: 39, title: 'Hawaii (the USA)', childPlaces: [], }, { id: 40, title: 'New Zealand', childPlaces: [], }, { id: 41, title: 'Vanuatu', childPlaces: [], }] }] }, { id: 42, title: 'Mặt Trăng', childPlaces: [{ id: 43, title: 'Rheita', childPlaces: [] }, { id: 44, title: 'Piccolomini', childPlaces: [] }, { id: 45, title: 'Tycho', childPlaces: [] }] }, { id: 46, title: 'Sao Hoả', childPlaces: [{ id: 47, title: 'Corn Town', childPlaces: [] }, { id: 48, title: 'Green Hill', childPlaces: [] }] }] };
Giờ giả sử bạn muốn thêm một cái nút để xoá một địa điểm mà bạn đã ghé thăm. Bạn sẽ làm như thế nào? Cập nhật state lồng nhau liên quan đến việc sao chép các object từ phần đã thay đổi. Xoá một địa điểm sâu sẽ liên quan đến việc sao chép toàn bộ chuỗi cha của nó. Đoạn code như vậy có thể rất dài dòng.
Nếu state quá lồng nhau để cập nhật dễ dàng, hãy xem xét làm cho nó “phẳng”. Dưới đây là một cách bạn có thể cấu trúc lại dữ liệu này. Thay vì một cấu trúc giống cây với mỗi place
có một mảng các địa điểm con của nó, bạn có thể làm cho mỗi địa điểm giữ một mảng các ID địa điểm con của nó. Sau đó map từng ID đến địa điểm tương ứng.
Cấu trúc mới này có thể khiến bạn nhớ đến việc xem một bảng cơ sở dữ liệu:
export const initialTravelPlan = { 0: { id: 0, title: '(Root)', childIds: [1, 42, 46], }, 1: { id: 1, title: 'Trái Đất', childIds: [2, 10, 19, 26, 34] }, 2: { id: 2, title: 'Châu Phi', childIds: [3, 4, 5, 6 , 7, 8, 9] }, 3: { id: 3, title: 'Botswana', childIds: [] }, 4: { id: 4, title: 'Egypt', childIds: [] }, 5: { id: 5, title: 'Kenya', childIds: [] }, 6: { id: 6, title: 'Madagascar', childIds: [] }, 7: { id: 7, title: 'Morocco', childIds: [] }, 8: { id: 8, title: 'Nigeria', childIds: [] }, 9: { id: 9, title: 'Nam Phi', childIds: [] }, 10: { id: 10, title: 'Châu Mỹ', childIds: [11, 12, 13, 14, 15, 16, 17, 18], }, 11: { id: 11, title: 'Argentina', childIds: [] }, 12: { id: 12, title: 'Brazil', childIds: [] }, 13: { id: 13, title: 'Barbados', childIds: [] }, 14: { id: 14, title: 'Canada', childIds: [] }, 15: { id: 15, title: 'Jamaica', childIds: [] }, 16: { id: 16, title: 'Mexico', childIds: [] }, 17: { id: 17, title: 'Trinidad and Tobago', childIds: [] }, 18: { id: 18, title: 'Venezuela', childIds: [] }, 19: { id: 19, title: 'Châu Á', childIds: [20, 21, 22, 23, 24, 25], }, 20: { id: 20, title: 'Trung Quốc', childIds: [] }, 21: { id: 21, title: 'Ấn Độ', childIds: [] }, 22: { id: 22, title: 'Singapore', childIds: [] }, 23: { id: 23, title: 'Hàn Quốc', childIds: [] }, 24: { id: 24, title: 'Thái Lan', childIds: [] }, 25: { id: 25, title: 'Việt Nam', childIds: [] }, 26: { id: 26, title: 'Châu Âu', childIds: [27, 28, 29, 30, 31, 32, 33], }, 27: { id: 27, title: 'Croatia', childIds: [] }, 28: { id: 28, title: 'Pháp', childIds: [] }, 29: { id: 29, title: 'Đức', childIds: [] }, 30: { id: 30, title: 'Italy', childIds: [] }, 31: { id: 31, title: 'Portugal', childIds: [] }, 32: { id: 32, title: 'Spain', childIds: [] }, 33: { id: 33, title: 'Thổ Nhĩ Kỳ', childIds: [] }, 34: { id: 34, title: 'Châu Đại Dương', childIds: [35, 36, 37, 38, 39, 40, 41], }, 35: { id: 35, title: 'Úc', childIds: [] }, 36: { id: 36, title: 'Bora Bora (French Polynesia)', childIds: [] }, 37: { id: 37, title: 'Easter Island (Chile)', childIds: [] }, 38: { id: 38, title: 'Fiji', childIds: [] }, 39: { id: 40, title: 'Hawaii (the USA)', childIds: [] }, 40: { id: 40, title: 'New Zealand', childIds: [] }, 41: { id: 41, title: 'Vanuatu', childIds: [] }, 42: { id: 42, title: 'Mặt Trăng', childIds: [43, 44, 45] }, 43: { id: 43, title: 'Rheita', childIds: [] }, 44: { id: 44, title: 'Piccolomini', childIds: [] }, 45: { id: 45, title: 'Tycho', childIds: [] }, 46: { id: 46, title: 'Sao Hoả', childIds: [47, 48] }, 47: { id: 47, title: 'Corn Town', childIds: [] }, 48: { id: 48, title: 'Green Hill', childIds: [] } };
Giờ khi state đã “phẳng” (còn được gọi là “chuẩn hoá”), việc cập nhật các mục lồng nhau trở nên dễ dàng hơn.
Để xoá một địa điểm bây giờ, bạn chỉ cần thực hiện hai cập nhật state:
- Xoá ID của nó khỏi mảng
childIds
của địa điểm cha. - Cập nhật object state gốc để nó không chứa địa điểm đó nữa.
Đây là một ví dụ về cách bạn có thể thực hiện điều đó:
import { useState } from 'react'; import { initialTravelPlan } from './places.js'; export default function TravelPlan() { const [plan, setPlan] = useState(initialTravelPlan); function handleComplete(parentId, childId) { const parent = plan[parentId]; // Tạo một phiên bản mới của địa điểm cha // mà không bao gồm ID con này. const nextParent = { ...parent, childIds: parent.childIds .filter(id => id !== childId) }; // Cập nhật object state gốc... setPlan({ ...plan, // ...để nó có cha đã cập nhật. [parentId]: nextParent }); } const root = plan[0]; const planetIds = root.childIds; return ( <> <h2>Địa điểm tham quan</h2> <ol> {planetIds.map(id => ( <PlaceTree key={id} id={id} parentId={0} placesById={plan} onComplete={handleComplete} /> ))} </ol> </> ); } function PlaceTree({ id, parentId, placesById, onComplete }) { const place = placesById[id]; const childIds = place.childIds; return ( <li> {place.title} <button onClick={() => { onComplete(parentId, id); }}> Hoàn thành </button> {childIds.length > 0 && <ol> {childIds.map(childId => ( <PlaceTree key={childId} id={childId} parentId={id} placesById={placesById} onComplete={onComplete} /> ))} </ol> } </li> ); }
Bạn có thể lồng state bao nhiêu cũng được, nhưng làm cho nó “phẳng” có thể giải quyết nhiều vấn đề. Nó giúp cập nhật state dễ dàng hơn, và đảm bảo bạn không có sự trùng lặp ở các phần khác nhau của một object lồng nhau.
Tìm hiểu sâu
Một cách lý tưởng, bạn cũng nên xoá các mục đã xoá (và các mục con của chúng!) khỏi object “bảng” để cải thiện việc sử dụng bộ nhớ. Phiên bản này thực hiện điều đó. Nó cũng sử dụng Immer để làm cho logic cập nhật ngắn gọn hơn.
{ "dependencies": { "immer": "1.7.3", "react": "latest", "react-dom": "latest", "react-scripts": "latest", "use-immer": "0.5.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "devDependencies": {} }
Đôi khi, bạn cũng có thể giảm thiểu việc lồng state bằng cách di chuyển một số state lồng vào các component con. Điều này hoạt động tốt cho state UI tạm thời mà không cần lưu trữ, như việc kiểm tra một item có được hover hay không.
Tóm tắt
- Nếu hai state luôn luôn cập nhật cùng nhau, hãy xem xét việc gộp chúng thành một.
- Chọn cẩn thận các biến state của bạn để tránh tạo ra các trạng thái “không thể xảy ra”.
- Cấu trúc state của bạn sao cho giảm khả năng bạn sẽ mắc lỗi khi cập nhật nó.
- Tránh state trùng lặp và dư thừa để bạn không cần phải đồng bộ chúng.
- Không đặt props vào state trừ khi bạn muốn ngăn cập nhật.
- Đối với UI như chọn lựa, giữ ID hoặc index trong state thay vì chính object đó.
- Nếu việc cập nhật state lồng nhau quá phức tạp, hãy thử làm phẳng nó.
Challenge 1 of 4: Fix một component không cập nhật
Component Clock
này nhận vào hai props: color
và time
. Khi bạn chọn một màu khác trong hộp chọn, component Clock
nhận một color
khác từ component cha của nó. Tuy nhiên, vì một lý do nào đó, màu hiển thị không cập nhật. Tại sao? Hãy fix lỗi này.
import { useState } from 'react'; export default function Clock(props) { const [color, setColor] = useState(props.color); return ( <h1 style={{ color: color }}> {props.time} </h1> ); }