選擇狀態結構

良好的狀態結構可以讓元件更易於修改和除錯,避免成為錯誤的根源。以下是在構建狀態時應考慮的一些技巧。

你將學到

  • 何時使用單個與多個狀態變數
  • 組織狀態時應避免的事項
  • 如何修復狀態結構的常見問題

狀態結構的原則

當你編寫一個包含狀態的元件時,你必須選擇使用多少個狀態變數以及它們的資料形狀。雖然即使狀態結構不佳也能編寫正確的程式,但有一些原則可以指導你做出更好的選擇。

  1. 將相關狀態分組。 如果你總是同時更新兩個或多個狀態變數,請考慮將它們合併成單個狀態變數。
  2. 避免狀態矛盾。 當狀態的結構方式可能導致多個狀態片段相互矛盾和“不一致”時,就會留下出錯的空間。盡量避免這種情況。
  3. 避免冗餘狀態。 如果你可以在渲染過程中從元件的 props 或其現有狀態變數計算某些資訊,則不應將該資訊放入該元件的狀態中。
  4. 避免狀態重複。 當相同的資料在多個狀態變數之間或嵌套物件內重複時,很難保持它們同步。盡可能減少重複。
  5. 避免深度嵌套狀態。 深度階層狀態更新起來不是很方便。盡可能以扁平的方式構建狀態。

這些原則的目標是*在不引入錯誤的情況下輕鬆更新狀態*。從狀態中移除冗餘和重複資料有助於確保所有片段保持同步。這類似於資料庫工程師可能希望 “正規化”資料庫結構 以減少錯誤的可能性。套用愛因斯坦的話,“讓你的狀態盡可能簡單——但不要過於簡化。”

現在讓我們看看這些原則如何在實踐中應用。

你可能有時不確定是使用單個還是多個狀態變數。

你應該這樣做嗎?

const [x, setX] = useState(0);
const [y, setY] = useState(0);

還是這樣?

const [position, setPosition] = useState({ x: 0, y: 0 });

從技術上講,你可以使用任何一種方法。但是如果兩個狀態變數總是同時更改,那麼將它們合併成單個狀態變數可能是一個好主意。 這樣你就不會忘記始終保持它們同步,例如下面這個例子,移動游標會更新紅點的兩個坐標

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>
  )
}

將資料分組到物件或陣列中的另一種情況是,當你不知道你需要多少個狀態片段時。例如,當你有一個使用者可以新增自定義欄位的表單時,這很有幫助。

陷阱

如果你的狀態變數是一個物件,請記住 你不能只更新其中一個欄位 而不顯式複製其他欄位。例如,你不能在上面的例子中執行 setPosition({ x: 100 }),因為它根本沒有 y 屬性!相反,如果你想單獨設定 x,你要麼執行 setPosition({ ...position, x: 100 }),要麼將它們分成兩個狀態變數並執行 setX(100)

避免狀態矛盾

這是一個飯店意見回饋表單,其中包含 isSendingisSent 狀態變數

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>Thanks for feedback!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>How was your stay at The Prancing Pony?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        Send
      </button>
      {isSending && <p>Sending...</p>}
    </form>
  );
}

// Pretend to send a message.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

雖然這段程式碼可以運作,但它為「不可能」的狀態留下了空間。例如,如果您忘記同時呼叫 setIsSentsetIsSending,您可能會遇到 isSendingisSent 同時為 true 的情況。您的元件越複雜,就越難理解發生了什麼事。

由於 isSendingisSent 永遠不應該同時為 true,因此最好將它們替換為一個 status 狀態變數,該變數可以是以下 _三種_ 有效狀態之一: 'typing'(初始)、'sending''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>Thanks for feedback!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>How was your stay at The Prancing Pony?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        Send
      </button>
      {isSending && <p>Sending...</p>}
    </form>
  );
}

// Pretend to send a message.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

您仍然可以宣告一些常數來提高可讀性。

const isSending = status === 'sending';
const isSent = status === 'sent';

但它們不是狀態變數,因此您不必擔心它們彼此不同步。

避免冗餘狀態

如果您可以元件的 props 或其現有的狀態變數在渲染過程中計算出某些資訊,您不應該將該資訊放入該元件的狀態中。

例如,使用這個表單。它可以運作,但您能在其中找到任何冗餘狀態嗎?

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>
    </>
  );
}

此表單具有三個狀態變數:firstNamelastNamefullName。但是,fullName 是冗餘的。您始終可以在渲染過程中從 firstNamelastName 計算出 fullName,因此請將其從狀態中移除。

您可以這樣做:

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>
    </>
  );
}

在這裡,fullName _不是_ 狀態變數。相反,它是在渲染過程中計算出來的。

const fullName = firstName + ' ' + lastName;

因此,變更處理程式不需要執行任何特殊操作來更新它。當您呼叫 setFirstNamesetLastName 時,您會觸發重新渲染,然後下一個 fullName 將根據新的資料計算出來。

深入探討

不要在狀態中鏡像 props

冗餘狀態的一個常見示例如下所示的程式碼:

function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);

在這裡,color 狀態變數初始化為 messageColor prop。問題是,如果父元件稍後傳遞不同的 messageColor 值(例如,'red' 而不是 'blue'),則 color _狀態變數_ 將不會更新! 狀態僅在第一次渲染期間初始化。

這就是為什麼在狀態變數中「鏡像」某些 prop 可能會導致混淆的原因。相反,請直接在您的程式碼中使用 messageColor prop。如果您想給它一個較短的名稱,請使用常數。

function Message({ messageColor }) {
const color = messageColor;

這樣它就不會與從父元件傳遞的 prop 不同步。

僅當您 _想要_ 忽略特定 prop 的所有更新時,「鏡像」props 到狀態才有意義。按照慣例,prop 名稱以 initialdefault 開頭,以闡明其新值將被忽略。

function Message({ initialColor }) {
// The `color` state variable holds the *first* value of `initialColor`.
// Further changes to the `initialColor` prop are ignored.
const [color, setColor] = useState(initialColor);

避免狀態重複

這個選單列表元件可讓您從多種旅行點心中選擇一種。

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

  return (
    <>
      <h2>What's your travel snack?</h2>
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.title}
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

目前,它將所選項目作為物件儲存在 selectedItem 狀態變數中。但是,這不太好:selectedItem 的內容與 items 列表中某個項目的物件相同。這表示項目本身的資訊在兩個地方重複了。

為什麼這是個問題?讓我們將每個項目都設為可編輯。

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', 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>What's your travel snack?</h2> 
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

請注意,如果您先點擊某個項目上的「選擇」,_然後_ 編輯它,輸入會更新,但底部的標籤不會反映編輯內容。這是因為您有重複的狀態,並且您忘記更新 selectedItem

雖然您也可以更新 selectedItem,但更簡單的解決方案是移除重複。在此範例中,您不是使用 selectedItem 物件(這會建立與 items 內物件的重複),而是在狀態中保存 selectedId,_然後_ 通過搜尋具有該 ID 的項目的 items 陣列來取得 selectedItem

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', 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>What's your travel snack?</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);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

狀態曾經像這樣重複:

  • 項目 = [{ id: 0, title: '椒鹽脆餅'}, ...]
  • selectedItem = {id: 0, title: '椒鹽脆餅'}

但在變更後會像這樣

  • 項目 = [{ id: 0, title: '椒鹽脆餅'}, ...]
  • selectedId = 0

重複的部分消失了,您只保留了必要的狀態!

現在,如果您編輯*選定的*項目,下方的訊息將立即更新。這是因為 setItems 觸發了重新渲染,並且 items.find(...) 會找到標題已更新的項目。您不需要在狀態中保存*選定的項目*,因為只有*選定的 ID* 是必要的。其餘部分可以在渲染期間計算。

避免深度巢狀狀態

想像一個由行星、大陸和國家組成的旅行計劃。您可能會想要使用巢狀物件和陣列來 structuring 其狀態,如下例所示

export const initialTravelPlan = {
  id: 0,
  title: '(Root)',
  childPlaces: [{
    id: 1,
    title: 'Earth',
    childPlaces: [{
      id: 2,
      title: 'Africa',
      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: 'South Africa',
        childPlaces: []
      }]
    }, {
      id: 10,
      title: 'Americas',
      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: 'Asia',
      childPlaces: [{
        id: 20,
        title: 'China',
        childPlaces: []
      }, {
        id: 21,
        title: 'India',
        childPlaces: []
      }, {
        id: 22,
        title: 'Singapore',
        childPlaces: []
      }, {
        id: 23,
        title: 'South Korea',
        childPlaces: []
      }, {
        id: 24,
        title: 'Thailand',
        childPlaces: []
      }, {
        id: 25,
        title: 'Vietnam',
        childPlaces: []
      }]
    }, {
      id: 26,
      title: 'Europe',
      childPlaces: [{
        id: 27,
        title: 'Croatia',
        childPlaces: [],
      }, {
        id: 28,
        title: 'France',
        childPlaces: [],
      }, {
        id: 29,
        title: 'Germany',
        childPlaces: [],
      }, {
        id: 30,
        title: 'Italy',
        childPlaces: [],
      }, {
        id: 31,
        title: 'Portugal',
        childPlaces: [],
      }, {
        id: 32,
        title: 'Spain',
        childPlaces: [],
      }, {
        id: 33,
        title: 'Turkey',
        childPlaces: [],
      }]
    }, {
      id: 34,
      title: 'Oceania',
      childPlaces: [{
        id: 35,
        title: 'Australia',
        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: 'Moon',
    childPlaces: [{
      id: 43,
      title: 'Rheita',
      childPlaces: []
    }, {
      id: 44,
      title: 'Piccolomini',
      childPlaces: []
    }, {
      id: 45,
      title: 'Tycho',
      childPlaces: []
    }]
  }, {
    id: 46,
    title: 'Mars',
    childPlaces: [{
      id: 47,
      title: 'Corn Town',
      childPlaces: []
    }, {
      id: 48,
      title: 'Green Hill',
      childPlaces: []      
    }]
  }]
};

現在,假設您要新增一個按鈕來刪除您已經去過的地方。您會怎麼做? 更新巢狀狀態 需要從變更的部分一路向上複製物件。刪除深度巢狀的地方將涉及複製其整個父級地方鏈。這樣的程式碼可能會非常冗長。

如果狀態巢狀太深而難以更新,請考慮將其「扁平化」。 您可以透過以下方式 restructuring 此資料。您可以讓每個地方保存一個*其子地方 ID* 的陣列,而不是每個 place 都有一個*其子地方* 的陣列的樹狀結構。然後儲存從每個地方 ID 到相應地方的映射。

這種資料 restructuring 可能會讓您想起資料庫表格

export const initialTravelPlan = {
  0: {
    id: 0,
    title: '(Root)',
    childIds: [1, 42, 46],
  },
  1: {
    id: 1,
    title: 'Earth',
    childIds: [2, 10, 19, 26, 34]
  },
  2: {
    id: 2,
    title: 'Africa',
    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: 'South Africa',
    childIds: []
  },
  10: {
    id: 10,
    title: 'Americas',
    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: 'Asia',
    childIds: [20, 21, 22, 23, 24, 25],   
  },
  20: {
    id: 20,
    title: 'China',
    childIds: []
  },
  21: {
    id: 21,
    title: 'India',
    childIds: []
  },
  22: {
    id: 22,
    title: 'Singapore',
    childIds: []
  },
  23: {
    id: 23,
    title: 'South Korea',
    childIds: []
  },
  24: {
    id: 24,
    title: 'Thailand',
    childIds: []
  },
  25: {
    id: 25,
    title: 'Vietnam',
    childIds: []
  },
  26: {
    id: 26,
    title: 'Europe',
    childIds: [27, 28, 29, 30, 31, 32, 33],   
  },
  27: {
    id: 27,
    title: 'Croatia',
    childIds: []
  },
  28: {
    id: 28,
    title: 'France',
    childIds: []
  },
  29: {
    id: 29,
    title: 'Germany',
    childIds: []
  },
  30: {
    id: 30,
    title: 'Italy',
    childIds: []
  },
  31: {
    id: 31,
    title: 'Portugal',
    childIds: []
  },
  32: {
    id: 32,
    title: 'Spain',
    childIds: []
  },
  33: {
    id: 33,
    title: 'Turkey',
    childIds: []
  },
  34: {
    id: 34,
    title: 'Oceania',
    childIds: [35, 36, 37, 38, 39, 40, 41],   
  },
  35: {
    id: 35,
    title: 'Australia',
    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: 'Moon',
    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: 'Mars',
    childIds: [47, 48]
  },
  47: {
    id: 47,
    title: 'Corn Town',
    childIds: []
  },
  48: {
    id: 48,
    title: 'Green Hill',
    childIds: []
  }
};

現在狀態是「扁平的」(也稱為「正規化」),更新巢狀項目變得更容易。

為了現在刪除一個地方,您只需要更新兩個層級的狀態

  • 其*父級*地方的更新版本應將已移除的 ID 從其 childIds 陣列中排除。
  • 根「表格」物件的更新版本應包含父級地方的更新版本。

以下是如何進行的範例

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];
    // Create a new version of the parent place
    // that doesn't include this child ID.
    const nextParent = {
      ...parent,
      childIds: parent.childIds
        .filter(id => id !== childId)
    };
    // Update the root state object...
    setPlan({
      ...plan,
      // ...so that it has the updated parent.
      [parentId]: nextParent
    });
  }

  const root = plan[0];
  const planetIds = root.childIds;
  return (
    <>
      <h2>Places to visit</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);
      }}>
        Complete
      </button>
      {childIds.length > 0 &&
        <ol>
          {childIds.map(childId => (
            <PlaceTree
              key={childId}
              id={childId}
              parentId={id}
              placesById={placesById}
              onComplete={onComplete}
            />
          ))}
        </ol>
      }
    </li>
  );
}

您可以根據需要巢狀狀態,但將其「扁平化」可以解決許多問題。它可以更輕鬆地更新狀態,並有助於確保巢狀物件的不同部分沒有重複。

深入探討

改善記憶體使用量

理想情況下,您也應該從「表格」物件中移除已刪除的項目(及其子項!)以改善記憶體使用量。這個版本會執行此操作。它也 使用 Immer 來簡化更新邏輯。

{
  "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": {}
}

有時,您也可以透過將一些巢狀狀態移至子組件來減少狀態巢狀。這適用於不需要儲存的臨時 UI 狀態,例如項目是否已滑鼠懸停。

重點回顧

  • 如果兩個狀態變數始終一起更新,請考慮將它們合併成一個。
  • 仔細選擇您的狀態變數,以避免建立「不可能的」狀態。
  • 以減少更新錯誤機會的方式來 structuring 您的狀態。
  • 避免冗餘和重複的狀態,這樣您就不需要保持它們同步。
  • 除非您特別想要防止更新,否則不要將 props 放入狀態中。
  • 對於像選擇這樣的 UI 模式,請在狀態中保留 ID 或索引,而不是物件本身。
  • 如果更新深度巢狀狀態很複雜,請嘗試將其扁平化。

挑戰 1 4:
修復未更新的組件

這個 Clock 組件接收兩個 props:colortime。當您在下拉式選單中選擇不同的顏色時,Clock 組件會從其父組件接收不同的 color prop。但是,由於某種原因,顯示的顏色沒有更新。為什麼?請解決這個問題。

import { useState } from 'react';

export default function Clock(props) {
  const [color, setColor] = useState(props.color);
  return (
    <h1 style={{ color: color }}>
      {props.time}
    </h1>
  );
}