深刻理解 form 系列(二)-- React 表單的優化

React Form

在構建 web 應用的時候,爲了採集用戶輸入,表單變成了咱們不可或缺的東西。大型項目中,若是沒有對錶單進行很好的抽象和封裝,隨着表單複雜度和數量的增長,處理表單將會變成一件使人頭疼的事情。在 react 裏面處理表單,一開始也並不容易。因此在這篇文章中,咱們會介紹一些簡單的實踐,讓你可以在 react 裏面更加輕鬆的使用表單。若是你對 HTML 表單的基礎掌握得不是太好,那麼我建議你先閱讀個人上一篇文章 深刻理解 HTML 表單html

好了,廢話很少說,讓咱們先來看一個簡單的例子。react

示例

LoginForm.jsgit

handleChange = evt => {
    this.setState({
      username: evt.target.value,
    });
  };

  render() {
    return (
      <form>
        <label>
          username:
          <input
            type="text"
            name="username"
            value={this.state.username}
            onChange={this.handleChange}
          />
        </label>
        <input
          type="submit"
          value="Submit"
        />
      </form>
    );
  }

在上面的例子中,咱們建立了一個輸入框,指望用戶在點擊 submit 以後,提交用戶輸入。github

移步 這裏
查看文章中的所有代碼

數據的抽象

對於每個表單元素來講, 除開 DOM 結構的不同,初始值, 錯誤信息, 是否被 touched, 是否 valid,這些數據都是必不可少的。因此,咱們能夠抽象一箇中間組件,將這些數據統一管理起來,而且適應不一樣的表單元素。這樣 Field 組件 就應運而生了。web

Field 做爲一箇中間層,包含表單元素的各類抽象。最基本的就是 Field 的名字對應的值
Field 不能單獨存在,由於 Field 的 value 都是來自傳入組件的 state, 傳入組件經過 setState 更新 state, 使 Field 的 value 發生變化redux

Field: {
   name: String,  // filed name, 至關於上面提到的 key
   value: String, // filed value
}

在實際狀況中, 還須要更多的數據來控制 Field 的表現行爲,好比 valid, invalid, touched 等。segmentfault

Field:{
   name: String,  // filed name, 至關於上面提到的 key
   value: String, // filed value
   label: String,
   error: String,
   initialValue: String,
   valid: Boolean,
   invalid: Boolean,
   visited: Boolean, // focused
   touched: Boolean, // blurred
   active: Boolean, // focusing
   dirty: Boolean, // 跟初始值不相同
   pristine: Boolean, // 跟初始值相同
   component: Component|Function|String, // 表單元素
}

點這裏瞭解 => Redux Form 對 Field 的抽象api

UI的抽象

Field 組件

  1. 做爲通用抽象, Field對外提供一致接口。 一致的接口可以使 Field 的使用起來更加的簡單。好比更新 checkbox 的時候,咱們更新的是它的 checked 屬性而不是 value 屬性,可是咱們能夠對 Field 進行封裝,對外所有提供 value 屬性,使開發變得更加容易。
  2. 做爲中間層, Field能夠起到攔截做用。 如先格式化傳入的 value,再將這個 value 傳遞給下層的組件,這樣全部下層組件獲得的都是格式化以後的值。

Field.js函數

static defaultProps = {
    component: Input,
  };
  
  render() {
    const { component, noLabel, label, ...otherProps } = this.props;
    return (
      <label>
        {!noLabel && <span>{label}</span>}
        {
          createElement(component, { ...otherProps })
        }
      </label>
    );
  }
上面的例子是 Field 組件的簡單實現。Field 對外提供了統一的 label 和 noLabel 接口,用來顯示或不顯示 label 元素。

Input 組件

建立Input 組件的關鍵點在於使它變得「可控」,也就是說它並不維護內部狀態。關於可控組件,接下來會介紹。測試

Input.js

handleChange = evt => {
    this.props.onChange(evt.target.value);
  };

  render() {
    return (
      <input {...this.props} onChange={this.handleChange} />
    );
  }

看上面的代碼,爲何不直接把 onChange 函數經過 props 傳進來呢?就像下面這樣

render() {
    return (
      <input {...this.props} onChange={this.props.onChange} />
    );
  }
實際上是爲了讓咱們從 onChange 回調中獲得 統一的 value, 這樣咱們在外部就不用去 care 到底是 取 event.target.value 仍是 event.target.checked.

優化後的 LoginForm 以下:

LoginForm.js

class LoginForm extends Component {
  state = {
    username: '',
  };
  handleChange = value => {
    this.setState({
      username: value,
    });
  };
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <Field
          label="username"
          name="username"
          value={this.state.username}
          onChange={this.handleChange}
        />
        <input
          type="submit"
          value="Submit"
        />
      </form>
    );
  }
}

可控組件與不可控組件

可控組件與不可控組件最大的區別就是:對內部狀態的維護與否

一個可控的 <input> 應該具備哪些特色?

  1. 經過 props 提供 value。可控組件並不維護本身的內部狀態,也就是外部提供什麼,就顯示什麼,因此組件可以經過 props 很好的控制起來
  2. 經過 onChange 更新value。
<input
      type="text"
      value={this.props.username}
      onChange={this.handleChange}
   />

點這裏瞭解 => React 可控組件與不可控組件

使用 React 高階組件進一步優化

在 LoinForm.js 中能夠看到,咱們對 setState 操做的依賴程度很高。若是在 form 中多添加一些 Field 組件,不難發現對於每個 Field,都須要重複 setState 操做。過多的 setState 會咱們的Form 組件變得不可控,增長維護成本。

仔細觀察上面的代碼,不難發現,在每一次 onChange 事件中,都是經過一個 keyvalue更新到 state 裏面。好比上面的例子中,咱們是經過 username 這個 key 去更新的。因此不難想到,利用高階組件,能夠不用在 LoginForm 裏面維護內部狀態。

高階組件在這裏就再也不展開了,我會在接下來的文章中專門來詳細介紹這一部份內容。

withState.js

const withState = (stateName, stateUpdateName, initialValue) =>
  BaseComponent =>
    class extends Component {
      state = {
        stateValue: initialValue,
      };

      updateState = (stateValue) => {
        this.setState({
          stateValue,
        });
      };

      render() {
        const { stateValue } = this.state;
        return createElement(BaseComponent, {
          ...this.props,
          [stateName]: stateValue,
          [stateUpdateName]: this.updateState,
        });
      }
    };
除了 state 以外,咱們能夠將 onChange, onSubmit 等事件處理函數也 extract 出去,這樣能夠進一步簡化咱們的 Form。

withHandlers.js

const withHandlers = handlers => BaseComponent =>
  class WithHandler extends Component {
    cachedHandlers = {};

    handlers = mapValues(
      handlers,
      (createHandler, handlerName) => (...args) => {
        const cachedHandler = this.cachedHandlers[handlerName];
        if (cachedHandler) {
          return cachedHandler(...args);
        }

        const handler = createHandler(this.props);
        this.cachedHandlers[handlerName] = handler;
        return handler(...args);
      }
    );

    componentWillReceiveProps() {
      this.cachedHandlers = {};
    }

    render() {
      return createElement(BaseComponent, {
        ...this.props,
        ...this.handlers,
      });
    }
  };

使用高階組件改造後的 LoginForm 以下:

LoginForm.js

const withLoginForm = _.flowRight(
  withState('username', 'onChange', ''),
  withHandlers({
    onChange: props => value => {
      props.onChange(value);
    },
    onSubmit: props => event => {
      event.preventDefault();
      console.log(props.username);
    },
  })
);

@withLoginForm
class LoginForm extends Component {
  static propTypes = {
    username: PropTypes.string,
    onChange: PropTypes.func,
    onSubmit: PropTypes.func,
  };

  render() {
    const { username, onChange, onSubmit } = this.props;
    return (
      <form onSubmit={onSubmit}>
        <Field
          label="username"
          name="username"
          value={username}
          onChange={onChange}
        />
        <input
          type="submit"
          value="Submit"
        />
      </form>
    );
  }
}
經過 composewithStatewithHandler 組合起來, 並應用到 Form 以後,跟以前比起來,LoginForm 已經簡化了不少。LoginForm 再也不本身維護內部狀態,變成了一個完徹底全的可控組件,不論是以後要對它寫測試仍是要重用它,都變得十分的輕鬆了。

點這裏瞭解 => Recompose

結語

對於複雜的項目來講,以上的抽象還遠遠不夠,在下一篇文章中,會介紹如何進一步讓你的 Form 變得更好用。

相關文章
相關標籤/搜索