React State Antipattern
- Deriving State
- Redundant State
如果可以透過派生出的資料就不應該多出 state 進行管理,例如:
function TripSummary() {
const [tripItems] = useState([
{ name: 'Flight', cost: 500 },
{ name: 'Hotel', cost: 300 },
]);
const [totalCost, setTotalCost] = useState(0); // ❌ Unnecessary state
useEffect(() => {
setTotalCost(tripItems.reduce((sum, item) => sum + item.cost, 0)); // ❌ Sync effect
}, [tripItems]);
return <div>Total: ${totalCost}</div>;
}多出了不必要的 setState 和 useEffect 額外進行管理可以被生出來的資料:
// ✅ Derive the value directly
const totalCost = tripItems.reduce((sum, item) => sum + item.cost, 0);另一個例子與第一個 Deriving State 類似,但可能會較難發現:
function HotelSelection() {
const [hotels] = useState([
{ id: 'h1', name: 'Grand Hotel', price: 200 },
{ id: 'h2', name: 'Budget Inn', price: 80 },
]);
const [selectedHotel, setSelectedHotel] = useState<Hotel | null>(null); // ❌ Stores entire object
const handleSelect = (hotel: Hotel) => {
setSelectedHotel(hotel); // ❌ Duplicates data from hotels array
};
return (
<div>
{selectedHotel && (
<div>
{selectedHotel.name} - ${selectedHotel.price}
</div>
)}
</div>
);
}將 selectedHotel 完整的儲存,看似沒問題,但是違反了單一資料來源的問題,假設在背後有個 interval 會去更新飯店的價格,但是 selectedHotel 並沒有被更新到,因完它完整的儲存了新的 Hotel,這極有可能會發生 Bug。
最好的方式是使用第一個派生的方式去處理:
const handleSelect = (hotelId: string) => {
setSelectedHotelId(hotelId); // ✅ Store minimal data
};
// ✅ Derive the full object when needed
const selectedHotel = hotels.find((h) => h.id === selectedHotelId);References
https://github.com/davidkpiano/frontend-masters-state-workshop