React入门教程(6)React表单处理及状态提升

表单

HTML表单元素与React中的其他DOM元素有所不同,因为表单元素生来就保留一些内部状态。例如,下面这个表单只接受一个唯一的name。

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

当用户提交表单时,HTML的默认行为会使这个表单跳转到一个新页面。在React中亦是如此。但大多数情况下,我们都会构造一个处理提交表单并可访问用户输入表单数据的函数。实现这一点的标准方法是使用一种称为“受控组件”的技术。

受控组件

在HTML当中,像<input>,<textarea>, 和 <select>这类表单元素会维持自身状态,并根据用户输入进行更新。但在React中,可变的状态通常保存在组件的状态属性中,并且只能用 setState()方法进行更新。

我们通过使react变成一种单一数据源的状态来结合二者。React负责渲染表单的组件仍然控制用户后续输入时所发生的变化。相应的,其值由React控制的输入表单元素称为“受控组件”。

例如,我们想要使上个例子中在提交表单时输出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 属性是在我们的表单元素上设置的,因此显示的值将始终为 React数据源上this.state.value 的值。由于每次按键都会触发 handleChange 来更新当前React的state,所展示的值也会随着不同用户的输入而更新。

使用"受控组件",每个状态的改变都有一个与之相关的处理函数。这样就可以直接修改或验证用户输入。例如,我们如果想限制输入全部是大写字母,我们可以将handleChange 写为如下:

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

textarea 标签

在HTML当中,<textarea> 元素通过子节点来定义它的文本内容

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

在React中,<textarea>会用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是在构造函数中初始化,这样文本区域就能获取到其中的文本。

select 标签

在HTML当中,<select>会创建一个下拉列表。例如这个HTML就创建了一个下拉列表的原型。

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

请注意,Coconut选项最初由于selected属性是被选中的。在React中,并不使用之前的selected属性,而在根select标签上用value属性来表示选中项。这在受控组件中更为方便,因为你只需要在一个地方来更新组件。例如:

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> 都十分类似 - 他们都通过传入一个value属性来实现对组件的控制。

file input 标签

在HTML当中,<input type="file"> 允许用户从他们的存储设备中选择一个或多个文件以提交表单的方式上传到服务器上, 或者通过 Javascript 的 File API 对文件进行操作 。

<input type="file" />

由于该标签的 value 属性是只读的, 所以它是 React 中的一个非受控组件。我们会把它和其他非受控组件一起在后面的章节进行详细的介绍。

多个输入的解决方法

当你有处理多个受控的input元素时,你可以通过给每个元素添加一个name属性,来让处理函数根据 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 上尝试。

注意我们如何使用ES6当中的计算属性名语法来更新与给定输入名称相对应的状态键:

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

相当于如下ES5语法

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

同样由于 setState() 自动将部分状态合并到当前状态,因此我们只需要使用发生变化的部分调用它。

受控组件的替代方法

有时使用受控组件可能很繁琐,因为您要为数据可能发生变化的每一种方式都编写一个事件处理程序,并通过一个组件来管理全部的状态。当您将预先存在的代码库转换为React或将React应用程序与非React库集成时,这可能变得特别烦人。在以上情况下,你或许应该看看非受控组件,这是一种表单的替代技术。

综合自定义表单校验案例

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) => {
    // 设置状态:是异步执行。
    this.setState({
      [e.target.name]: e.target.value
    }, () => {
      this.validateInput();
    });
  }

  handlerSubmit = (e) => {
    e.preventDefault();
    // 第一: 做表单的校验
    this.validateInput();
    // 第二: 做表单提交到后台ajax请求
  };

  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;

状态提升

使用 react 经常会遇到几个组件需要共用状态数据的情况。这种情况下,我们最好将这部分共享的状态提升至他们最近的父组件当中进行管理。我们来看一下具体如何操作吧

我们一个计数的父组件,两个按钮组件,两个按钮组件分别对父组件中的数据进行添加和减少操作。

// 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 具有强大的组合模型,我们建议使用组合而不是继承来复用组件之间的代码。

在本节中,我们将围绕几个 React 新手经常使用继承解决的问题,我们将展示如何用组合来解决它们。

包含关系

一些组件不能提前知道它们的子组件是什么。这对于 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 标签内的任何内容都将通过 children 属性传入 FancyBorder。由于 FancyBorder 在一个 <div> 内渲染了 {props.children},所以被传递的所有元素都会出现在最终输出中。

虽然不太常见,但有时你可能需要在组件中有多个入口,这种情况下你可以使用自己约定的属性而不是 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 元素都是对象,所以你可以像任何其他元素一样传递它们。

特殊实例

有时我们认为组件是其他组件的特殊实例。例如,我们会说 WelcomeDialogDialog 的特殊实例。

在 React 中,这也是通过组合来实现的,通过配置属性用较特殊的组件来渲染较通用的组件。

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 上试试。

组合对于定义为类的组件同样适用:

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. 官网文档
  2. 老马React视频地址: https://ke.qq.com/course/379234?tuin=1eb4a0a4
  3. AICODER官网地址:https://www.aicoder.com/
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容