咱們或許不須要 React 的 Form 組件

在上一篇小甜點 《咱們或許不須要 classnames 這個庫》 中, 咱們 簡單的使用了一些語法代替了 classnames 這個庫html

如今咱們調整一下難度, 移除 React 中相對比較複雜的組件: Form 組件react

在移除 Form 組件以前, 咱們現須要進行一些思考, 爲何會有 Form 組件及 Form 組件和 React 狀態管理的關係git

注意, 接下來的內容很是容易讓 React 開發人員感到不適, 而且極具爭議性github

單向數據流及受控組件

Angular, Vue, 都有雙向綁定, 而 React 官方文檔也爲一個 input 標籤的雙向綁定給了一個官方方案 - 受控組件:redux

reactjs.org/docs/forms.…架構

本文中提到的代碼均可以直接粘貼至項目中進行驗證.dom

// 如下是官方的受控組件例子:
class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}
複製代碼

相信寫過 React 項目的人都已經很是熟練, 受控組件就是: 把一個 input 的 value 和 onChange 關聯到某一個狀態中.函數

很長一段時間, 使用受控組件, 咱們都會受到如下幾個困惑:post

  1. 針對較多表單內容的頁面, 編寫受控組件繁瑣
  2. 跨組件的受控組件須要使用 onChange 等 props 擊鼓傳花, 層層傳遞, 這種狀況下作表單聯動就會變得麻煩

社區對以上的解決方案是提供一些表單組件, 比較經常使用的有:性能

包括我本身也編寫過 Form 組件

它們解決了如下幾個問題:

  1. 跨組件獲取表單內容
  2. 表單聯動
  3. 根據條件去執行或修改表單組件的某些行爲, 如:
    • 表單校驗
    • props屬性控制
    • ref獲取函數並執行

其實這些表單都是基於 React 官方受控組件的封裝, 其中 Antd Form 及 no-form 都是參考咱們的先知 Dan Abramov 的理念:

單向數據流, 狀態管理至頂而下; 這樣能夠確保整個架構數據的同步, 增強項目的穩定性; 它知足如下 4 個特色:

  1. 不阻斷數據流
  2. 時刻準備渲染
  3. 沒有單例組件
  4. 隔離本地狀態

Dan Abramov 具體的文章在此處: 編寫有彈性的組件

行業內極力推崇單向數據流的方案, 我在以前的項目中一直以 redux + immutable 做爲項目管理, 項目也一直穩定運行, 直到 React-Hooks 的方案出現(這是另外的話題).

單向數據流的特色是用計算時間換開發人員的時間, 咱們舉一個小例子說明:

若是當前組件樹中有 100 個 組件, 其中50個組件被 connect 注入了狀態, 那麼當發起一個 dispatch 行爲, 須要更新1個組件, 這50個組件的會被更新, 咱們須要在 mapPropsToState 中過濾沒必要要的狀態數據, 而後在使用 immutable 在 shouldComponentUpdate 中進行較低開銷的判斷, 以攔截另外49個沒必要要更新的組件.

單向數據流的好處是咱們永遠只須要維護最頂部的狀態, 減小了系統的混亂程度.

缺點也是明顯的: 咱們須要額外的判斷是否更新的開銷

大部分 Form 表單獲取數據的思路也是一個內聚的單向數據流, 每次 onChange 就修改 Form 中的 state, 子組件經過註冊 context, 獲取及更新相應的值. 這是知足 Dan Abramov 的設計理念的.

而 react-final-form 沒有使用以上模式, 而是經過發佈訂閱, 把每一個組件的更新加入訂閱, 根據行爲進行相應的更新, 按照以上的例子, 它們是如此運做:

若是當前組件樹中有 100 個 組件, 其中50個組件被 Form 標記了, 那麼當發起一個 input 行爲, 須要更新1個組件, 會找到這一個組件, 在內部進行 setState, 並把相應的值更新到 Form 中的 data 中.

這種設計有沒有違背 React 的初衷呢? 我認爲是沒有的, 由於 Form 維護的內容是局部的, 而不是總體的, 咱們只須要讓整個 Form 不脫離數據流的管理便可.

經過 react-final-form 這個組件的例子我想明白了一件事情:

  1. 單向數據流是幫咱們更容易的管理, 可是並非表示非單向數據流狀態就必定混亂, 就如 react-final-form 組件所管理的表單狀態.

  2. 既然 react-final-form 能夠這麼設計, 咱們爲何不能設計局部的, 脫離受控組件的範疇的表單?

好的, 能夠進入正題了:

表單內部的組件能夠脫離受控組件存在, 只須要讓表單自己爲受控組件

使用 form 標籤代替 React Form 組件

咱們用一個簡單的例子實現最開始 React 官方的受控組件的示例代碼:

class App extends React.Component {
  formDatas = {};

  handleOnChange = event => {
    // 在input事件中, 咱們將dom元素的值存儲起來, 用於表單提交
    this.formDatas[event.target.name] = event.target.value;
  };

  handleOnSubmit = event => {
    console.log('formDatas: ', this.formDatas);
    event.preventDefault();
  };

  render() {
    return (
      <form onChange={this.handleOnChange} onSubmit={this.handleOnSubmit}>
        <input name="username" />
        <input name="password" />
        <button type="submit" />
      </form>
    );
  }
}
複製代碼

這是最簡單的獲取值, 存儲到一個對象中, 咱們會一步步描述如何脫離受控組件進行值和狀態管理, 可是爲了後續的代碼更加簡潔, 咱們使用 hooks 完成以上行爲:

獲取表單內容

function App() {
  // 使用 useRef 來存儲數據, 這樣能夠防止函數每次被從新執行時沒法存儲變量
  const { current: formDatas } = React.useRef({});

  // 使用 useCallback 來聲明函數, 減小組件重繪時從新聲明函數的開銷
  const handleOnChange = React.useCallback(event => {
    // 在input事件中, 咱們將dom元素的值存儲起來, 用於表單提交
    formDatas[event.target.name] = event.target.value;
  }, []);

  const handleOnSubmit = React.useCallback(event => {
    // 提交表單
    console.log('formDatas: ', formDatas);
    event.preventDefault();
  }, []);

  return (
    <form onChange={handleOnChange} onSubmit={handleOnSubmit}>
      <input name="username" />
      <input name="password" />
      <button type="submit" />
    </form>
  );
}
複製代碼

接下來的代碼都會在此基礎上, 使用 hooks 語法編寫

跨組件獲取表單內容

咱們不須要作任何處理, form 標籤本來就能夠獲取其內部的全部表單內容

// 子組件, form標籤同樣能夠獲取相應的輸入
function PasswordInput(){
  return <div>
    <p>密碼:</p>
    <input name="password" />
  </div>
}

function App() {
  const { current: formDatas } = React.useRef({});

  const handleOnChange = React.useCallback(event => {
    formDatas[event.target.name] = event.target.value;
  }, []);

  const handleOnSubmit = React.useCallback(event => {
    console.log('formDatas: ', formDatas);
    event.preventDefault();
  }, []);

  return (
    <form onChange={handleOnChange} onSubmit={handleOnSubmit}>
      <input name="username" />
      <PasswordInput />
      <button type="submit" />
    </form>
  );
}
複製代碼

表單聯動 \ 校驗

如今咱們在以前的基礎上實現一個需求:

若是密碼長度大於8, 將用戶名和密碼重置爲默認值

咱們經過 form, 將 input 的 DOM 元素存儲起來, 再在一些狀況進行 DOM 操做, 直接更新, 代碼以下:

function App() {
  const { current: formDatas } = React.useRef({});
  const { current: formTargets } = React.useRef({});

  const handleOnChange = React.useCallback(event => {
    // 在input事件中, 咱們將dom元素的值存儲起來, 用於表單提交
    formDatas[event.target.name] = event.target.value;
    // 在input事件中, 咱們將dom元素儲存起來, 接下來根據條件修改value
    formTargets[event.target.name] = event.target;

    // 若是密碼長度大於8, 將用戶名和密碼重置爲默認值
    if (formTargets.password && formDatas.password.length > 8) {
      // 修改DOM元素的value, 更新視圖
      formTargets.password.value = formTargets.password.defaultValue;
      // 若是存儲過
      if (formTargets.username) {
        // 修改DOM元素的value, 更新視圖
        formTargets.username.value = formTargets.username.defaultValue;
      }
    }
  }, []);

  const handleOnSubmit = React.useCallback(event => {
    console.log('formDatas: ', formDatas);
    event.preventDefault();
  }, []);

  return (
    <form onChange={handleOnChange} onSubmit={handleOnSubmit}>
      <input defaultValue="hello" name="username" />
      <input defaultValue="" name="password" />
      <button type="submit" />
    </form>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));
複製代碼

如上述代碼, 咱們很簡單的實現了表單的聯動, 由於直接操做 DOM, 因此整個組件並無從新執行 render, 這種更新方案的性能是極佳的(HTML的極限).

在寫 React 的時候咱們都很是忌諱直接操做 DOM, 這是由於, 若是咱們操做了 DOM, 可是經過React對Node的Diff以後, 又進行更新, 可能會覆蓋掉以前操做 DOM 的一些行爲. 可是若是咱們確保這些 DOM 並非受控組件, 那麼就不會發生以上狀況.

它會有什麼問題麼? 當其餘行爲觸發 React 重繪時, 這些標籤內的值會被清空嗎?

明顯是不會的, 只要 React 的組件沒有被銷燬, 即使重繪, React 也只是獲取到 dom對象修改其屬性:

function App() {
  const { current: formDatas } = React.useRef({});
  const { current: formTargets } = React.useRef({});
  const [value, setValue] = React.useState(10);

  // 咱們這裏每隔 500ms 自動更新, 而且重繪咱們的輸入框的字號
  React.useEffect(() => {
    setInterval(() => {
      setValue(v => v + 1);
    }, 300);
  }, []);

  const handleOnChange = React.useCallback(event => {
    formDatas[event.target.name] = event.target.value;
    formTargets[event.target.name] = event.target;

    if (formTargets.password && formDatas.password.length > 8) {
      formTargets.password.value = formTargets.password.defaultValue;
      if (formTargets.username) {
        formTargets.username.value = formTargets.username.defaultValue;
      }
    }
  }, []);

  const handleOnSubmit = React.useCallback(event => {
    console.log('formDatas: ', formDatas);
    event.preventDefault();
  }, []);

  return (
    <form onChange={handleOnChange} onSubmit={handleOnSubmit}>
      <p>{value}</p>
      <input defaultValue="hello" name="username" />
      {/* p 標籤會一直被 setState 更新, 字號逐步增大, 咱們輸入的值並無丟失 */}
      <input defaultValue="" name="password" style={{ fontSize: value }} />
      <button type="submit" />
    </form>
  );
}
複製代碼

可是, 若是標籤被銷燬了, 非受控組件的值就不會被保存

如下例子, input 輸入了值以後, 被消耗再被重繪, 此時以前 input 的值已經丟失了

function App() {
  const { current: formDatas } = React.useRef({});
  const { current: formTargets } = React.useRef({});
  const [value, setValue] = React.useState(0);

  React.useEffect(() => {
    setInterval(() => {
      setValue(v => v + 1);
    }, 500);
  }, []);

  const handleOnChange = React.useCallback(event => {
    formDatas[event.target.name] = event.target.value;
    formTargets[event.target.name] = event.target;
  }, []);

  const handleOnSubmit = React.useCallback(event => {
    console.log('formDatas: ', formDatas);
    event.preventDefault();
  }, []);

  return (
    <form onChange={handleOnChange} onSubmit={handleOnSubmit}>
      {/* 若是 value 是 5 的整數倍, input 會被銷燬, 已輸入的值會丟失 */}
      {value % 5 !== 0 && <input name="username" />}
      {/* 咱們可使用 defaultValue 去讀取歷史的值, 讓重繪時讀取以前輸入的值 */}
      {value % 5 !== 0 && <input defaultValue={formDatas.password} name="password" />}
      {/* 若是可能, 咱們最好使用 display 代替條件渲染 */}
      <input name="code" style={{ display: value % 5 !== 0 ? 'block' : 'none' }} />
      <button type="submit" />
    </form>
  );
}
複製代碼

如代碼中的註釋所述:

  1. 若是 input 被銷燬, 已輸入的值會丟失
  2. 咱們可使用 defaultValue 去讀取歷史的值, 讓重繪時讀取以前輸入的
  3. 若是可能, 咱們最好使用 display 代替條件渲

好了, 咱們在瞭解了直接操做 DOM 的優勢和弊端以後, 咱們繼續實現表單常見的其餘行爲.

跨層級組件通訊

根據條件執行某子組件的函數, 咱們只須要獲取該組件的ref便可, 可是若是涉及到多層級的組件, 這就會很麻煩.

傳統 Form 組件會提供一個 FormItem, FormItem 會獲取 context, 從而提供跨多級組件的通訊

而咱們如何既然已經獲取到 DOM 元素了, 咱們只須要在 DOM 元素上捆綁事件, 就能夠無痛的作到跨層級的通訊. 這個行爲徹底違反咱們平時編寫 React 的思路和常規操做, 可是經過以前咱們對 "標籤銷燬" 的理解, 一般可使它在可控的範圍內.

咱們看看實現的代碼案例:

// 此爲子子組件
function SubInput() {
  const ref = React.useRef();

  React.useEffect(() => {
    if (ref.current) {
      // 在DOM元素上捆綁一個函數, 此函數能夠執行此組件的上下文事件
      ref.current.saved = name => {
        console.log('do saved by: ', name);
      };
    }
  }, [ref]);

  return (
    <div> {/* 獲取表單的DOM元素 */} <input ref={ref} name="sub-input" /> </div> ); } // 此爲子組件, 僅引用了子子組件 function Input() { return ( <div> <SubInput /> </div> ); } function App() { const { current: formDatas } = React.useRef({}); const { current: formTargets } = React.useRef({}); const handleOnChange = React.useCallback(event => { formDatas[event.target.name] = event.target.value; formTargets[event.target.name] = event.target; // 直接經過dom元素上的屬性, 獲取子子組件的事件 event.target.saved && event.target.saved(event.target.name); }, []); const handleOnSubmit = React.useCallback(event => { console.log('formDatas: ', formDatas); event.preventDefault(); }, []); return ( <form onChange={handleOnChange} onSubmit={handleOnSubmit}> {/* 咱們應用了某個子子組件, 而且沒用傳遞任何 props, 也沒有捆綁任何 context, 沒有獲取ref */} <Input /> </form> ); } 複製代碼

根據此例子咱們能夠看到, 使用 html 的 form 標籤,就能夠完成咱們絕大部分的 Form 組件的場景, 並且開發效率和執行效率都更高.

爭議

經過操做 DOM, 咱們能夠很自然解決一些 React 很是棘手才能解決的問題. 誠然這有點像在刀尖上跳舞, 可是此文中給出了一些會遇到的問題及解決方案.

我很是歡迎對此類問題的討論, 有哪些還會遇到的問題, 若是能清晰的將其原理及緣由描述並回復到此文, 那是對全部閱讀者的幫助.

寫在最後

請不要被教條約束, 試試挑戰它.

相關文章
相關標籤/搜索