React入門教程(6)React表單處理及狀態(tài)提升

表單

HTML表單元素與React中的其他DOM元素有所不同,因?yàn)楸韱卧厣鷣砭捅A粢恍﹥?nèi)部狀態(tài)。例如,下面這個(gè)表單只接受一個(gè)唯一的name。

<form>
  <label>
    Name:
    <input type="text" name="name" />
  </label>
  <input type="submit" value="Submit" />
</form>

當(dāng)用戶提交表單時(shí),HTML的默認(rèn)行為會(huì)使這個(gè)表單跳轉(zhuǎn)到一個(gè)新頁面。在React中亦是如此。但大多數(shù)情況下,我們都會(huì)構(gòu)造一個(gè)處理提交表單并可訪問用戶輸入表單數(shù)據(jù)的函數(shù)。實(shí)現(xiàn)這一點(diǎn)的標(biāo)準(zhǔn)方法是使用一種稱為“受控組件”的技術(shù)。

受控組件

在HTML當(dāng)中,像<input>,<textarea>, 和 <select>這類表單元素會(huì)維持自身狀態(tài),并根據(jù)用戶輸入進(jìn)行更新。但在React中,可變的狀態(tài)通常保存在組件的狀態(tài)屬性中,并且只能用 setState()方法進(jìn)行更新。

我們通過使react變成一種單一數(shù)據(jù)源的狀態(tài)來結(jié)合二者。React負(fù)責(zé)渲染表單的組件仍然控制用戶后續(xù)輸入時(shí)所發(fā)生的變化。相應(yīng)的,其值由React控制的輸入表單元素稱為“受控組件”。

例如,我們想要使上個(gè)例子中在提交表單時(shí)輸出name,我們可以寫成“受控組件”的形式:

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

在 CodePen 上嘗試。

由于 value 屬性是在我們的表單元素上設(shè)置的,因此顯示的值將始終為 React數(shù)據(jù)源上this.state.value 的值。由于每次按鍵都會(huì)觸發(fā) handleChange 來更新當(dāng)前React的state,所展示的值也會(huì)隨著不同用戶的輸入而更新。

使用"受控組件",每個(gè)狀態(tài)的改變都有一個(gè)與之相關(guān)的處理函數(shù)。這樣就可以直接修改或驗(yàn)證用戶輸入。例如,我們?nèi)绻胂拗戚斎肴渴谴髮懽帜福覀兛梢詫?code>handleChange 寫為如下:

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

textarea 標(biāo)簽

在HTML當(dāng)中,<textarea> 元素通過子節(jié)點(diǎn)來定義它的文本內(nèi)容

<textarea>
  Hello there, this is some text in a text area
</textarea>

在React中,<textarea>會(huì)用value屬性來代替。這樣的話,表單中的<textarea> 非常類似于使用單行輸入的表單:

class EssayForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: 'Please write an essay about your favorite DOM element.'
    };

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

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

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

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

注意this.state.value是在構(gòu)造函數(shù)中初始化,這樣文本區(qū)域就能獲取到其中的文本。

select 標(biāo)簽

在HTML當(dāng)中,<select>會(huì)創(chuàng)建一個(gè)下拉列表。例如這個(gè)HTML就創(chuàng)建了一個(gè)下拉列表的原型。

<select>
  <option value="grapefruit">Grapefruit</option>
  <option value="lime">Lime</option>
  <option selected value="coconut">Coconut</option>
  <option value="mango">Mango</option>
</select>

請(qǐng)注意,Coconut選項(xiàng)最初由于selected屬性是被選中的。在React中,并不使用之前的selected屬性,而在根select標(biāo)簽上用value屬性來表示選中項(xiàng)。這在受控組件中更為方便,因?yàn)槟阒恍枰谝粋€(gè)地方來更新組件。例如:

class FlavorForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: 'coconut'};

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

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

  handleSubmit(event) {
    alert('Your favorite flavor is: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Pick your favorite La Croix flavor:
          <select value={this.state.value} onChange={this.handleChange}>
            <option value="grapefruit">Grapefruit</option>
            <option value="lime">Lime</option>
            <option value="coconut">Coconut</option>
            <option value="mango">Mango</option>
          </select>
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

在 CodePen 上嘗試。

總之,<input type="text">, <textarea>, 和 <select> 都十分類似 - 他們都通過傳入一個(gè)value屬性來實(shí)現(xiàn)對(duì)組件的控制。

file input 標(biāo)簽

在HTML當(dāng)中,<input type="file"> 允許用戶從他們的存儲(chǔ)設(shè)備中選擇一個(gè)或多個(gè)文件以提交表單的方式上傳到服務(wù)器上, 或者通過 Javascript 的 File API 對(duì)文件進(jìn)行操作 。

<input type="file" />

由于該標(biāo)簽的 value 屬性是只讀的, 所以它是 React 中的一個(gè)非受控組件。我們會(huì)把它和其他非受控組件一起在后面的章節(jié)進(jìn)行詳細(xì)的介紹。

多個(gè)輸入的解決方法

當(dāng)你有處理多個(gè)受控的input元素時(shí),你可以通過給每個(gè)元素添加一個(gè)name屬性,來讓處理函數(shù)根據(jù) event.target.name的值來選擇做什么。

例如:

class Reservation extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isGoing: true,
      numberOfGuests: 2
    };

    this.handleInputChange = this.handleInputChange.bind(this);
  }

  handleInputChange(event) {
    const target = event.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;

    this.setState({
      [name]: value
    });
  }

  render() {
    return (
      <form>
        <label>
          Is going:
          <input
            name="isGoing"
            type="checkbox"
            checked={this.state.isGoing}
            onChange={this.handleInputChange} />
        </label>
        <br />
        <label>
          Number of guests:
          <input
            name="numberOfGuests"
            type="number"
            value={this.state.numberOfGuests}
            onChange={this.handleInputChange} />
        </label>
      </form>
    );
  }
}

在 CodePen 上嘗試。

注意我們?nèi)绾问褂肊S6當(dāng)中的計(jì)算屬性名語法來更新與給定輸入名稱相對(duì)應(yīng)的狀態(tài)鍵:

this.setState({
  [name]: value
});

相當(dāng)于如下ES5語法

var partialState = {};
partialState[name] = value;
this.setState(partialState);

同樣由于 setState() 自動(dòng)將部分狀態(tài)合并到當(dāng)前狀態(tài),因此我們只需要使用發(fā)生變化的部分調(diào)用它。

受控組件的替代方法

有時(shí)使用受控組件可能很繁瑣,因?yàn)槟獮閿?shù)據(jù)可能發(fā)生變化的每一種方式都編寫一個(gè)事件處理程序,并通過一個(gè)組件來管理全部的狀態(tài)。當(dāng)您將預(yù)先存在的代碼庫轉(zhuǎn)換為React或?qū)eact應(yīng)用程序與非React庫集成時(shí),這可能變得特別煩人。在以上情況下,你或許應(yīng)該看看非受控組件,這是一種表單的替代技術(shù)。

綜合自定義表單校驗(yàn)案例

import React, { Component } from 'react';

class FormSub extends Component {
  constructor(opt) {
    super(opt);
    this.state = {
      Title: 'hi',
      Validate: {
        Title: {
          required: true,
          minLen: 6,
          maxLen: 10,
          validate: true,
          msg: '*ToDo不能為空!'
        }
      }
    }
  }

  handlerChange = (e) => {
    // 設(shè)置狀態(tài):是異步執(zhí)行。
    this.setState({
      [e.target.name]: e.target.value
    }, () => {
      this.validateInput();
    });
  }

  handlerSubmit = (e) => {
    e.preventDefault();
    // 第一: 做表單的校驗(yàn)
    this.validateInput();
    // 第二: 做表單提交到后臺(tái)ajax請(qǐng)求
  };

  validateInput() {
    let { Title, Validate } = this.state;
    let tempValidate = false;
    const len = Title.length;
    const min = Validate.Title.minLen;
    const max = Validate.Title.maxLen;
    if(len >= min && len <= max) {
      tempValidate = true;
    }

    this.setState(preState => {
      return Object.assign({}, preState, {
        Validate: {
          Title: Object.assign({}, preState.Validate.Title,{
            validate: tempValidate,
          })
        }
      });
    })
  }

  render() {
    return (
      <form onSubmit={this.handlerSubmit}>
        <label>
          ToDo:
          <input 
            type="text"
            name="Title"
            onChange={this.handlerChange}
            value={this.state.Title}
          />
          {
            !this.state.Validate.Title.validate &&
            <span 
              style={{color: 'red'}}
            >
              {this.state.Validate.Title.msg}
            </span>
          }
        </label>
        <br/>
        <input type="submit" value="提交"/>
      </form>
    );
  }
}

export default FormSub;

狀態(tài)提升

使用 react 經(jīng)常會(huì)遇到幾個(gè)組件需要共用狀態(tài)數(shù)據(jù)的情況。這種情況下,我們最好將這部分共享的狀態(tài)提升至他們最近的父組件當(dāng)中進(jìn)行管理。我們來看一下具體如何操作吧

我們一個(gè)計(jì)數(shù)的父組件,兩個(gè)按鈕組件,兩個(gè)按鈕組件分別對(duì)父組件中的數(shù)據(jù)進(jìn)行添加和減少操作。

// Counter.js 父組件
import React, { Component } from 'react';

import ButtonAdd from './ButtonAdd';
import ButtonMinus from './ButtonMinus';

class Counter extends Component {
  constructor(option) {
    super(option);
    this.state = { num: 0, age: 19 };
  }
  minusCount(num, e) {
    this.setState((preState) => {
      return { num: preState.num - num }
    });
  }
  addCount(num, e) {
    this.setState((preState) => {
      return { num: preState.num + num }
    });
  }
  render() {
    return (
      <div>
        <p>parent: { this.state.num } -{ this.state.age }</p>
        <hr />
        <ButtonAdd addCount={ this.addCount.bind(this) } num={ this.state.num } />
        <ButtonMinus minusCount={ this.minusCount.bind(this) } num={ this.state.num }  />
      </div>
    );
  }
}

export default Counter;

// 子組件 添加按鈕組件
import React, { Component } from 'react';

class ButtonAdd extends Component {
  render() {
    return (
      <div>
        <span>child:state {this.props.num}</span>
        <button onClick={ () => {
          this.props.addCount(1);
        }}>
          +1
        </button>
      </div>
    );
  }
}

export default ButtonAdd;

// 子組件:  減少按鈕組件
import React, { Component } from 'react';

class ButtonMinus extends Component {
  render() {
    return (
      <div>
        <span>child:state { this.props.num }</span>
        <button onClick={ () => {
          this.props.minusCount(1);
        }}>
          -1
        </button>
      </div>
    );
  }
}

export default ButtonMinus;

組合與props.children

React 具有強(qiáng)大的組合模型,我們建議使用組合而不是繼承來復(fù)用組件之間的代碼。

在本節(jié)中,我們將圍繞幾個(gè) React 新手經(jīng)常使用繼承解決的問題,我們將展示如何用組合來解決它們。

包含關(guān)系

一些組件不能提前知道它們的子組件是什么。這對(duì)于 SidebarDialog 這類通用容器尤其常見。

我們建議這些組件使用 children 屬性將子元素直接傳遞到輸出。

function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}

這樣做還允許其他組件通過嵌套 JSX 來傳遞子組件。

function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Thank you for visiting our spacecraft!
      </p>
    </FancyBorder>
  );
}

在 CodePen 上試試。

<FancyBorder> JSX 標(biāo)簽內(nèi)的任何內(nèi)容都將通過 children 屬性傳入 FancyBorder。由于 FancyBorder 在一個(gè) <div> 內(nèi)渲染了 {props.children},所以被傳遞的所有元素都會(huì)出現(xiàn)在最終輸出中。

雖然不太常見,但有時(shí)你可能需要在組件中有多個(gè)入口,這種情況下你可以使用自己約定的屬性而不是 children

function SplitPane(props) {
  return (
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}

function App() {
  return (
    <SplitPane
      left={
        <Contacts />
      }
      right={
        <Chat />
      } />
  );
}

在 CodePen 上試試。

類似 <Contacts /><Chat /> 這樣的 React 元素都是對(duì)象,所以你可以像任何其他元素一樣傳遞它們。

特殊實(shí)例

有時(shí)我們認(rèn)為組件是其他組件的特殊實(shí)例。例如,我們會(huì)說 WelcomeDialogDialog 的特殊實(shí)例。

在 React 中,這也是通過組合來實(shí)現(xiàn)的,通過配置屬性用較特殊的組件來渲染較通用的組件。

function Dialog(props) {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        {props.title}
      </h1>
      <p className="Dialog-message">
        {props.message}
      </p>
    </FancyBorder>
  );
}

function WelcomeDialog() {
  return (
    <Dialog
      title="Welcome"
      message="Thank you for visiting our spacecraft!" />
  );
}

在 CodePen 上試試。

組合對(duì)于定義為類的組件同樣適用:

function Dialog(props) {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        {props.title}
      </h1>
      <p className="Dialog-message">
        {props.message}
      </p>
      {props.children}
    </FancyBorder>
  );
}

class SignUpDialog extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.handleSignUp = this.handleSignUp.bind(this);
    this.state = {login: ''};
  }

  render() {
    return (
      <Dialog title="Mars Exploration Program"
              message="How should we refer to you?">
        <input value={this.state.login}
               onChange={this.handleChange} />
        <button onClick={this.handleSignUp}>
          Sign Me Up!
        </button>
      </Dialog>
    );
  }

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

  handleSignUp() {
    alert(`Welcome aboard, ${this.state.login}!`);
  }
}

在 CodePen 上試試。

參考

  1. 官網(wǎng)文檔
  2. 老馬React視頻地址: https://ke.qq.com/course/379234?tuin=1eb4a0a4
  3. AICODER官網(wǎng)地址:https://www.aicoder.com/
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容