組件設計 —— 從新認識受控與非受控組件

從新定義受控與非受控組件的邊界

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

在非受控組件中, 一般業務調用方只需傳入一個初始默認值即可使用該組件。以 Input 組件爲例:react

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

// 調用方
function Demo() {
  return <Input defaultValue={1} />
}
複製代碼

在受控組件中, 數值的展現與變動則分別由組件的 statesetState 接管。一樣以 Input 組件爲例:git

// 組件提供方
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} /> 的最初調用方式:github

// 組件提供方
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 組件是一個非受控組件。值得一提的是, 以非受控組件的使用方式去調用受控組件是一種反模式, 在下文中會分析其中的弊端。dom

如何作到無論對於組件提供方仍是調用方 Input 組件都爲受控組件呢? 提供方讓出控制權便可, 調整代碼以下codesandbox:spa

// 組件提供方
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)} />
}
複製代碼

通過上述代碼的推演後, 歸納以下: 受控以及非受控組件的邊界劃分取決於當前組件對於子組件值的變動是否擁有控制權。如如有則該子組件是當前組件的受控組件; 如若沒有則該子組件是當前組件的非受控組件。code

職能範圍

基於調用方對於受控組件擁有控制權這一認知, 所以受控組件相較非受控組件能賦予調用方更多的定製化職能。這一思路與軟件開發中的開放/封閉原則有殊途同歸之妙, 同時讓筆者受益不淺的 Inversion of Control 也是相似的思想。component

藉助受控組件的賦能, 以 Input 組件爲例, 好比調用方能夠更爲自由地對值進行校驗限制, 又好比在值發生變動時執行一些額外邏輯。cdn

// 組件提供方
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)}
  />
}
複製代碼

所以綜合基礎組件擴展性通用性的考慮, 受控組件的職能相較非受控組件更加寬泛, 建議優先使用受控組件來構建基礎組件。xml

反模式 —— 以非受控組件的使用方式調用受控組件

首先何謂反模式? 筆者將其總結爲增大隱性 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

相關文章
相關標籤/搜索