1. 程式人生 > >元件設計 —— 重新認識受控與非受控元件

元件設計 —— 重新認識受控與非受控元件

重新定義受控與非受控元件的邊界

React 官網中對非受控元件與受控元件作了如圖中下劃線的邊界定義。一經推敲, 該定義是缺乏了些完整性嚴謹性的, 比如針對非表單元件(彈框、輪播圖)如何劃分受控與非受控的邊界? 又比如非受控元件是否真的如文案上所說的資料的展示與變更都由 dom 自身接管呢?

在非受控元件中, 通常業務呼叫方只需傳入一個初始預設值便可使用該元件。以 Input 元件為例:

// 元件提供方
function Input({ defaultValue }) {
  return <input defaultValue={defaultValue} />
}

// 呼叫方
function Demo() {
  return <Input defaultValue={1} />
}

在受控元件中, 數值的展示與變更則分別由元件的 statesetState 接管。同樣以 Input 元件為例:

// 元件提供方
function Input() {
  const [value, setValue] = React.useState(1)
  return <input value={value} onChange={e => setValue(e.target.value)} />
}

// 呼叫方
function Demo() {
  return <Input />
}

有意思的一個問題來了, Input 元件到底是受控的還是非受控的? 我們甚至還可以對程式碼稍加改動成 <Input defaultValue={1} />

的最初呼叫方式:

// 元件提供方
function Input({ defaultValue }) {
  const [value, setValue] = React.useState(defaultValue)
  return <input value={value} onChange={e => setValue(e.target.value)} />
}

// 呼叫方
function Demo() {
  return <Input defaultValue={1} />
}

儘管此時 Input 元件本身是一個受控元件, 但與之相對的呼叫方失去了更改 Input 元件值的控制權

, 所以對呼叫方而言, Input 元件是一個非受控元件。值得一提的是, 以非受控元件的使用方式去呼叫受控元件是一種反模式, 在下文中會分析其中的弊端。

如何做到不管對於元件提供方還是呼叫方 Input 元件都為受控元件呢? 提供方讓出控制權即可, 調整程式碼如下codesandbox:

// 元件提供方
function Input({ value, onChange }) {
  return <input value={value} onChange={onChange} />
}

// 呼叫方
function Demo() {
  const [value, setValue] = React.useState(1)
  return <Input value={value} onChange={e => setValue(e.target.value)} />
}

經過上述程式碼的推演後, 概括如下: 受控以及非受控元件的邊界劃分取決於當前元件對於子元件值的變更是否擁有控制權。如若有則該子元件是當前元件的受控元件; 如若沒有則該子元件是當前元件的非受控元件。

職能範圍

基於呼叫方對於受控元件擁有控制權這一認知, 因此受控元件相較非受控元件能賦予呼叫方更多的定製化職能。這一思路與軟體開發中的開放/封閉原則有異曲同工之妙, 同時讓筆者受益匪淺的 Inversion of Control 也是類似的思想。

藉助受控元件的賦能, 以 Input 元件為例, 比如呼叫方可以更為自由地對值進行校驗限制, 又比如在值發生變更時執行一些額外邏輯。

// 元件提供方
function Input({ value, onChange }) {
  return <input value={value} onChange={onChange} />
}

// 呼叫方
function Demo() {
  const [value, setValue] = React.useState(1)
  return <Input value={value} onChange={e =>
    // 只支援數值的變更
    if (/\D/.test(e.target.value)) return
    setValue(e.target.value)}
  />
}

因此綜合基礎元件擴充套件性通用性的考慮, 受控元件的職能相較非受控元件更加寬泛, 建議優先使用受控元件來構建基礎元件。

反模式 —— 以非受控元件的使用方式呼叫受控元件

首先何謂反模式? 筆者將其總結為增大隱性 bug 出現概率的模式, 該模式是最佳實踐的對立經驗。如若使用了反模式就不得不花更多的精力去避免潛在 bug。官網對反模式也有很好的概括總結。

緣何上文提到以非受控元件的使用方式去呼叫受控元件是一種反模式? 觀察 Input 元件的第一行程式碼, 其將 defaultValue 賦值給 value, 這種將 props 賦值給 state 的賦值行為在一定程度上會增加某些隱性 bug 的出現概率。

比如在切換導航欄的場景中, 恰巧兩個導航中傳進元件的 defaultValue 是相同的值, 在導航切換的過程中便會將導航一中的 Input 的狀態值帶到導航二中, 這顯然會讓使用方感到困惑。codesandbox

// 元件提供方
function Input({ defaultValue }) {
  // 反模式
  const [value, setValue] = React.useState(defaultValue);
  React.useEffect(() => {
    setValue(defaultValue);
  }, [defaultValue]);
  return <input value={value} onChange={e => setValue(e.target.value)} />;
}

// 呼叫方
function Demo({ defaultValue }) {
  return <Input defaultValue={defaultValue} />;
}

function App() {
  const [tab, setTab] = React.useState(1);
  return (
    <>
      {tab === 1 ? <Demo defaultValue={1} /> : <Demo defaultValue={1} />}
      <button onClick={() => (tab === 1 ? setTab(2) : setTab(1))}>
        切換 Tab
      </button>
    </>
  );
}

如何避免使用該反模式同時有效解決問題呢? 官方提供了兩種較為優質的解法, 將其留給大家作為思考。

  1. 方法一: 使用完全受控元件(更為推薦)
  2. 方法二: 使用完全非受控元件 + key

歡迎關注 personal blog