React State Antipattern

  1. Deriving State
  2. 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>;
}

多出了不必要的 setStateuseEffect 額外進行管理可以被生出來的資料:

// ✅ 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