Props、State、Refs 与表单处理

前言

在前面的章节中我们已经对于 React 和 JSX 有初步的认识,我们也了解到 React Component 事实上可以视为显示 UI 的一个状态机(state machine),而这个状态机根据不同的 state(透过 setState() 修改)和 props(由父元素传入),Component 会出现对应的显示结果。本章将使用 React 官网首页上的范例(使用 ES6+)来更进一步说明 Props 和 State 特性及在 React 如何进行事件和表单处理。

Props

首先我们使用 React 官网上的 A Simple Component 来说明 props 的使用方式。由于传入组件的 name 属性为 Mark,故以下程式码将会在浏览器显示 Hello, Mark。针对传入的 props 我们也有验证和预设值的设计,让我们撰写的组件可以更加稳定健壮(robust)。

HTML Markup:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>A Component Using External Plugins</title>
</head>
<body>
<!-- 这边方便使用 CDN 方式引入 react 、 react-dom 进行讲解,实务上和实战教学部分我们会使用 webpack -->
<script src="https://fb.me/react-15.1.0.js"></script>
<script src="https://fb.me/react-dom-15.1.0.js"></script>
  <div id="app"></div>
    <script src="./app.js"></script>
</body>
</html>

app.js,使用 ES6 Class Component 写法:

class HelloMessage extends React.Component {
    // 若是需要绑定 this.方法或是需要在 constructor 使用 props,定义 state,就需要 constructor。若是在其他方法(如 render)使用 this.props 则不用一定要定义 constructor
    constructor(props) {
        // 对于 OOP 物件导向程式设计熟悉的读者应该对于 constructor 建构子的使用不陌生,事实上它是 ES6 的语法糖,骨子里还是 prototype based 物件导向程式语言。透过 extends 可以继承 React.Component 父类别。super 方法可以呼叫继承父类别的建构子
        super(props);
        this.state = {}
    }
    // render 是唯一必须的方法,但如果是单纯 render UI 建议使用 Functional Component 写法,效能较佳且较简洁
    render() {
        return (
            <div>Hello {this.props.name}</div>
        )
    }
}

// PropTypes 验证,若传入的 props type 不是 string 将会显示错误
HelloMessage.propTypes = {
  name: React.PropTypes.string,
}

// Prop 预设值,若对应 props 没传入值将会使用 default 值 Zuck
HelloMessage.defaultProps = {
 name: 'Zuck',
}

ReactDOM.render(<HelloMessage name="Mark" />, document.getElementById('app'));

关于 React ES6 class constructor super() 解释可以参考 React ES6 class constructor super()

使用 Functional Component 写法:

// Functional Component 可以视为 f(d) => UI,根据传进去的 props 绘出对应的 UI。注意这边 props 是传入函式的参数,因此取用 props 不用加 this
const HelloMessage = (props) => (
    <div>Hello {props.name}</div>
);

// PropTypes 验证,若传入的 props type 不是 string 将会显示错误
HelloMessage.propTypes = {
  name: React.PropTypes.string,
}

// Prop 预设值,若对应 props 没传入值将会使用 default 值 Zuck。用法等于 ES5 的 getDefaultProps
HelloMessage.defaultProps = {
 name: 'Zuck',
}

ReactDOM.render(<HelloMessage name="Mark" />, document.getElementById('app'));

在 jsbin 上的范例:

A Component Using External Plugins on jsbin.com

State

接下来我们将使用 A Stateful Component 这个范例来讲解 State 的用法。在 React Component 可以自己管理自己的内部 state,并用 this.state 来存取 state。当 setState() 方法更新了 state 后将重新呼叫 render() 方法,重新绘制 component 内容。以下范例是一个每 1000 毫秒(等于1秒)就会加一的累加器。由于这个范例是 Stateful Component 因此仅使用 ES6 Class Component,而不使用 Functional Component。

HTML Markup:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>A Component Using External Plugins</title>
</head>
<body>
<script src="https://fb.me/react-15.1.0.js"></script>
<script src="https://fb.me/react-dom-15.1.0.js"></script>
  <div id="app"></div>
    <script src="./app.js"></script>
</body>
</html>

app.js:

class Timer extends React.Component {
    constructor(props) {
        super(props);
        // 与 ES5 React.createClass({}) 不同的是 component 内自定义的方法需要自行绑定 this context,或是使用 arrow function
        this.tick = this.tick.bind(this);
        // 初始 state,等于 ES5 中的 getInitialState
        this.state = {
            secondsElapsed: 0,
        }
    }
    // 累加器方法,每一秒被呼叫后就会使用 setState() 更新内部 state,让 Component 重新 render
    tick() {
        this.setState({secondsElapsed: this.state.secondsElapsed + 1});
    }
    // componentDidMount 为 component 生命周期中阶段 component 已插入节点的阶段,通常一些非同步操作都会放置在这个阶段。这便是每1秒钟会去呼叫 tick 方法
    componentDidMount() {
        this.interval = setInterval(this.tick, 1000);
    }
    // componentWillUnmount 为 component 生命周期中 component 即将移出插入的节点的阶段。这边移除了 setInterval 效力
    componentWillUnmount() {
        clearInterval(this.interval);
    }
    // render 为 class Component 中唯一需要定义的方法,其回传 component 欲显示的内容
    render() {
        return (
          <div>Seconds Elapsed: {this.state.secondsElapsed}</div>
        );
    }
}

ReactDOM.render(<Timer />, document.getElementById('app'));

关于 Javascript this 用法可以参考 Javascript:this用法整理

事件处理(Event Handle)

在前面的内容我们已经学会如何使用 props 和 state,接下来我们要更进一步学习在 React 内如何进行事件处理。下列将使用 React 官网的 An Application 当做例子,实作出一个简单的 TodoApp。

HTML Markup:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>A Component Using External Plugins</title>
</head>
<body>
<script src="https://fb.me/react-15.1.0.js"></script>
<script src="https://fb.me/react-dom-15.1.0.js"></script>
  <div id="app"></div>
    <script src="./app.js"></script>
</body>
</html>

app.js:

// TodoApp 组件中包含了显示 Todo 的 TodoList 组件,Todo 的内容透过 props 传入 TodoList 中。由于 TodoList 仅单纯 Render UI 不涉及内部 state 操作是 stateless component,所以使用 Functional Component 写法。需要特别注意的是这边我们用 map function 来迭代 Todos,需要留意的是每个迭代的元素必须要有 unique key 不然会发生错误(可以用自定义 id,或是使用 map function 的第二个参数 index)
const TodoList = (props) => (
    <ul>
        {
            props.items.map((item) => (
                <li key={item.id}>{item.text}</li>
            ))
        }
    </ul>
)

// 整个 App 的主要组件,这边比较重要的是事件处理的部份,内部有
class TodoApp extends React.Component {
    constructor(props) {
        super(props);
        this.onChange = this.onChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
        this.state = {
            items: [],
            text: '',
        }
    }
    onChange(e) {
        this.setState({text: e.target.value});
    }
    handleSubmit(e) {
        e.preventDefault();
        const nextItems = this.state.items.concat([{text: this.state.text, id: Date.now()}]);
        const nextText = '';
        this.setState({items: nextItems, text: nextText});
    }
    render() {
        return (
          <div>
            <h3>TODO</h3>
            <TodoList items={this.state.items} />
            <form onSubmit={this.handleSubmit}>
              <input onChange={this.onChange} value={this.state.text} />
              <button>{'Add #' + (this.state.items.length + 1)}</button>
            </form>
          </div>
        );
    }
}

ReactDOM.render(<TodoApp />, document.getElementById('app'));

以上介绍了 React 事件处理的部份,除了 onChangeonSubmit 外,React 也封装了常用的事件处理,如 onClick 等。若想更进一步了解有哪些可以使用的事件处理方法可以参考 官网的 Event System

Refs 与表单处理

上面介绍了 props(传入后就不能修改)、state(随着使用者互动而改变)和事件处理机制后,我们将接续介绍如何在 React 中进行表单处理。同样我们使用 React 官网范例 A Component Using External Plugins 进行介绍。由于 React 可以容易整合外部的 libraries(例如:jQuery),本范例将使用 remarkable 结合 ref 属性取出 DOM Value 值(另外比较常用的作法是使用 onChange 事件处理方式处理表单内容),让使用者可以使用 Markdown 语法的所见即所得编辑器(editor)。

HTML Markup(除了引入 reactreact-dom 还要用 CDN 方式引入 remarkable 这个 Markdown 语法 parser 套件,记得如果没有使用 Webpack 或是 browserify + babelify 等工具需要引入 babel-standalone 浏览器解析 ES6 语法并于引入 script 加上 type="text/babel"):

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>A Component Using External Plugins</title>
</head>
<body>
<script src="https://fb.me/react-15.1.0.js"></script>
<script src="https://fb.me/react-dom-15.1.0.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.18.1/babel.min.js"></script>
<script src="https://cdn.jsdelivr.net/remarkable/1.6.2/remarkable.min.js"></script>
  <div id="app"></div>
    <script type="text/babel" src="./app.js"></script>
</body>
</html>

app.js:

class MarkdownEditor extends React.Component {
    constructor(props) {
        super(props);
        this.handleChange = this.handleChange.bind(this);
        this.rawMarkup = this.rawMarkup.bind(this);
        this.state = {
            value: 'Type some *markdown* here!',
        }
    }
    handleChange() {
        this.setState({value: this.refs.textarea.value});
    }
    // 将使用者输入的 Markdown 语法 parse 成 HTML 放入 DOM 中,React 通常使用 virtual DOM 作为和 DOM 沟通的中介,不建议直接由操作 DOM。故使用时的属性为 dangerouslySetInnerHTML
    rawMarkup() {
        const md = new Remarkable();
        return { __html: md.render(this.state.value) };
    }
    render() {
        return (
          <div className="MarkdownEditor">
            <h3>Input</h3>
            <textarea
              onChange={this.handleChange}
              ref="textarea"
              defaultValue={this.state.value} />
            <h3>Output</h3>
            <div
              className="content"
              dangerouslySetInnerHTML={this.rawMarkup()}
            />
          </div>
        );
    }
}

ReactDOM.render(<MarkdownEditor />, document.getElementById('app'));

总结

以上透过几个 React 官网首页上的范例介绍了 Props 和 State 特性及在 React 如何进行事件和表单处理这些 React 中核心的问题,若还不熟悉的读者建议重新亲自动手照着范例中的程式码敲过一遍,也可以使用像 jsbin 这样所见即所得的工具来练习,更能熟悉相关语法和 API 喔!接下来我们将探讨 Component 的生命周期。

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

推荐阅读更多精彩内容