本文译自React官方文档
开始学习React最简单的方式是参看CodePen上的Hello World实例。 您不需要安装任何东西,只要在浏览器中打开该链接并随我们学习该例子。如果想要使用本地开发环境,查看安装页面。 最简单的React实例如下:
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
这段代码在页面上渲染了一个Hello World的header。 接下来将逐步向您介绍如何使用React。我们将了解React程序的构建块-元素和组件。 掌握他们后,您将可以通过多块小段的可复用代码创建出复杂的应用。
React是一个JavaScript库,您需要明白JavaScript的基本语法。
在例子中,我们也会使用一些ES6的语言特性。由于其相对较新,我们会保守的使用它。
但您最好熟悉箭头函数,类,模板字符串,let
和const
声明。
您可以通过Babel REPL查看ES6的编译结果。
首先看看以下代码段:
const element = <h1>Hello, world!</h1>;
这个有趣的标签表达式既不是一个字符串,也不是一个HTML标签。 它叫做JSX,是JavaScript的扩展。我们推荐在React中使用JSX描述UI。 JSX也许会使你想起某种模板语言,它可以充分利用JavaScript的特性。 JSX创建React元素。我们将再下一章节学习如何将其渲染至DOM。接下来,我们将学习JSX的基本用法。
在JSX中,您可以通过 {} 的方式嵌入任何JavaScript表达式。 如 2+2, user.firstName, 和 formatName(user) 等均为有效的表达式:
function formatName(user) {
return user.firstName + ' ' + user.lastName;
}
const user = {
firstName: 'Harper',
lastName: 'Perez'
};
const element = (
<h1>
Hello, {formatName(user)}!
</h1>
);
ReactDOM.render(
element,
document.getElementById('root')
);
从可读性的角度考虑,我们将JSX分成了多行。但这并不是强制性的。 当需要分行时,我们推荐使用圆括号 () 将其括起,从而避免JS的自动插入机制ASI
通过编译后,JSX表达式变成了常规的JavaScript对象。 这意味着您可以使用JSX中使用 if 语句和 for 循环等:
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}
您可以使用引号 "" 将字符串指定为属性:
const element = <div tabIndex="0"></div>;
也可以使用括号 {} 嵌入JavaScript表达式作为属性:
const element = <img src={user.avatarUrl}></img>;
但是,当通过JavaScript表达式为属性赋值时,不要使用双引号。 否则,JSX会将其 视为一个字符串而非JavaScript表达式。 对于字符串,我们可以直接使用 "" 将其作为属性值;对于表达式,我们可以通过 {} 将其作为属性值。 但是 不要将其混用 。
当某tag不含值时,可以直接用 /> 将其关闭:
const element = <img src={user.avatarUrl} />;
JSX的标记可以包含子元素:
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
注意,由于相对HTML而言,JSX更加类似于JavaScript, React DOM使用驼峰命名代替HTML中的属性名。 __例如,JSX中使用 className 而非 class , 使用 tabIndex 而非 tabindex __
在JSX中直接嵌入用户输入是安全的:
const title = response.potentiallyMaliciousInput;
// This is safe:
const element = <h1>{title}</h1>;
在执行渲染前,React DOM会默认对任何嵌入的值进行编码。 这可以避免应用被注入,可以避免XSS攻击。
Babel将JSX编译为 React.createElement() 调用。 以下两种写法是一致的:
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
React.createElement() 会执行一些语法方面的检查,但其核心功能是创建一个如下的对象:
const element = {
...
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world'
}
...
};
这些对象被称为"React元素"。我们可以将其视为我们想要在屏幕上表现的元素的描述。React读取这些对象并利用他们构建DOM并保持更新。 我们将在下一部分学习将React元素渲染成DOM。
Tip: 为了ES6和JSX都能在编辑器中高亮显示,我们推荐您将您的编辑器设置为"Babel"语法方案。
在React应用中,元素是最小的构建单位。
元素用于描述你想在屏幕上看到的东西:
const element = <h1>Hello, world</h1>;
不同于浏览器的DOM元素,React元素是简单对象(Plainj Object),并且创建代价很小。React DOM负责更新DOM,使其与React元素的一致。
注意: 一个更广为人知的概念-"组件(Components)"可能会让你与"元素"混淆。我们将在下一部分介绍组件。元素是用于建立组件的"原料"之一
假设在你的HTML文件中的某处有一个div:
<div id="root"></div>
我们将其称为根元素-其内部所有事物都将由React DOM进行管理。
采用React创建的应用通常有一个根DOM节点。如果是想要将React结合入某个已有的应用,您可以根据需要创建任意多个根DOM节点。
利用ReactDOM.render()将一个React元素渲染至根DOM节点:
const element = <h1>Hello, World</h1>;
ReactDOM.render(
element,
document.getElementById('root');
);
这段代码会在页面上显示"Hello World"
React元素是的不可变的。一旦创建完成一个元素后,您无法改变其子元素或属性。元素就好比是电影中的某一帧:其代表了某个时间点的UI。 根据我们至今所学,更新UI的唯一方式是创建一个新元素并将其传递个ReactDOM.render(): 如下面这个时钟实例所示:
function tick(){
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);
}
setInterval(tick, 1000);
在CodePen中尝试 其通过setInterval()每隔一秒调用一次ReactDOM.render()
注意: 实际上,大多React应用只调用一次ReactDOM.render()。在下一部分我们将学习如何将这些代码封装至有状态的组件
React DOM会将元素及其子元素与先前状态进行比较,并且只会更新状态变动的元素。 你可以通过浏览器工具查看上一个实例
尽管我们每秒都创建了一个用于描述整个UI树的元素,但只有文本节点的内容被React DOM更新了。 根据我们的经验,"考虑UI全部时刻的状态而不是如何随时间改变它"能够避免许多问题。
组件使你能够将UI划分为独立的,可复用的个体。
从概念上来说,组件类似于JavaScript中的函数。
他们接受任意输入(这些输入被称为属性-props)并返回React元素,描述其在屏幕上应该如何显示。
定义组件最简单的方式是写一个JavaScript函数:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
这个函数是一个有效的React组件-其接受一个单一的,拥有数据的props
对象作为参数并返回一个React元素。
由于他们在字面上看来是函数,我们将这类组件成为"函数式组件"(Functional)
您也可以使用ES6的class定义一个组件:
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
以上两种方式对React而言是一样的。
我们将在下一部分讨论class拥有的一些额外的特性。
在此之前,由于函数式组件更简洁,我们将使用函数式组件。
之前,我们只遇到过代表DOM标签的React元素:
const element = <div />;
然而,元素也能够代表用户定义的组件:
const element = <Welcome name="Sara" />;
当React发现某个代表用户定义组件的元素时,它将把JSX的属性作为一个单独的对象传递给这个组件。
我们将这个对象称为"属性(props)"。
比如,以下代码将在页面上渲染"Hello, Sara":
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Sara" />;
ReactDOM.render(
element,
document.getElementById('root')
);
在CodePen中尝试
在这个例子中:
- 我们将
<Welcome name="Sara" />
作为参数调用了ReactDOM.render()
- React将
{name: 'Sara'}
作为属性调用了Welcome组件 - Welcome组件返回一个
<h1>Hello, Sara</h1>
元素 - React DOM高效的更新DOM-使其与
<h1>Hello, Sara</h1>
匹配
注意
为组件命名时首字母应大写。
比如说,<div />
代表一个DOM标签,但<Welcome />
则代表一个组件。
组件可以在输出中引用其他组件,这使我们能够对任何级别的粒度划分执行组件抽象。一个按钮、一个表单,一个对话框-在React应用中,这些通常都被作为一个组件。
例如,我们可以创建一个App组件用于多次渲染Welcome
:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
在CodePen中尝试
通常来说,新的React应用在最顶层有一个App组件。但是,如果您是将React引入至某个现有的应用中,你从可以使用如Button这样的小组件开始,自底向上逐步替换。
注意
组件比如返回一个单独的根元素。这也是我们为<Welcome />
元素外面包裹上一层<div>
的原因
不要担心将组件分割成更细粒度的组件。
例如,考虑如下组件:
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<img className="Avatar"
src={props.author.avatarUrl}
alt={props.author.name}
/>
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
在CodePen中尝试
该组件接受"作者(一个对象)", "文字(一个字符串)", 和一个"日期"作为属性,并在一个社交媒体网站上描述了评论。
由于过多的嵌套,这个组件难以被替换和复用。我们可以考虑对其进行分解。
首先,我们可以提取出Avatar:
function Avatar(props) {
return (
<img className="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
);
}
我们将user
(一个更通用的名字)而非author
作为其属性-Avatar不需要知道其是否是用于Comment的
我们建议从组件的角度而非其具体的应用场景对组件的属性进行命名。
现在可以对Comment
组件做一下简化:
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<Avatar user={props.author} />
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
接下来,我们接着提取出一个UserInfo
组件-该组件用于将Avatar
组件放在用户旁边:
function UserInfo(props) {
return (
<div className="UserInfo">
<Avatar user={props.user} />
<div className="UserInfo-name">
{props.user.name}
</div>
</div>
);
现在Comment组件可以进一步简化:
function Comment(props) {
return (
<div className="Comment">
<UserInfo user={props.author} />
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
在CodePen中尝试
提取组件的工作在开始时看起来意义不大,但在大型应用中,一个能复用的组件库能够大大的减轻开发成本。
一个好的经验是-如果你的某个组件要用到几次(如按钮,Avatar等),或其拥有一定的复杂度(App, FeedStory, Commen等),则有必要考虑是否需要将其作为可复用组件进行提取。
无论将组件声明为一个函数或一个类,其属性均是不可变的。考虑以下函数sum
:
function sum(a, b) {
return a + b;
}
这种函数被称为纯函数-他们不企图改变输入的值;对于相同的输入,总是返回相同的结果。
作为对比,下面这个函数不是纯函数(其改变了他的输入):
function withdraw(account, amount) {
account.total -= amount;
}
React非常灵活,但它有一个严格的规定:
所有的React组件必须要像纯函数一样去接受他们的props
当然,应用的UI是动态的,会随着时间改变。在下一部分,我们将介绍一个新的概念-状态(state
)。不同于属性,状态允许组件对其进行修改。
回顾之前学习的时钟的例子,元素用于描述我们想要在屏幕上看到的内容。
我们调用ReactDOM.render()
改变渲染输出:
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);
}
setInterval(tick, 1000);
在CodePen中尝试
在本部分,我们将学习如何使Clock
组件变得可重用,它会建立自己的定时器并每秒自动进行更新。
我们可以先从封装Clock
的样式着手:
function Clock(props) {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
);
}
function tick() {
ReactDOM.render(
<Clock date={new Date()} />,
document.getElementById('root')
);
}
setInterval(tick, 1000);
在CodePen中尝试
但是,这里漏了一个很重要的地方:"Clock
建立一个定时器并每秒更新UI"这一行为事实上应该是由Clock自己实现的(而不是将new Date作为属性传入)。
理想情况下它应该是这样的:
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
为了完成这个目的,我们应该为Clock
组件增加__状态(state)__。
状态与属性(props)类似,但它是私有的,且完全由组件控制。
我们之前提到过,被定义为class的组件拥有额外的特性,本地状态就是其中之一-一个只有类组件拥有的特性。
我们可以通过5步将Clock
组件从函数式转换为类组件:
- 创建一个继承自
React.Component
的ES6 class - 为其增加一个叫做
render()
的方法 - 将函数式组件的内容移至
render()
方法中 - 在移动的过程中,将原来的
props
转为this.props
- 删除剩余的空函数声明
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
在CodePen中尝试
Clock
组件现在从函数式组件变成了类组件,这使我们能够使用诸如state和Lifecycle Hooks等额外属性。
我们通过以下3个步骤将date
从props变成state:
- 在
render()
中使用this.state.date
替换this.props.date
:
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
- 增加一个构造函数初始化
this.state
:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
注意-我们向父类的构造函数传递了props:
constructor(props) {
super(props);
this.state = {date: new Date()};
}
类组件应该总是在构造函数中执行super(props)
这一过程
3. 删除<Clock />
中的date
属性:
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
我们会在稍后将定时器相关的代码增加至组件内部。
目前替换后的结果如下:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
在CodePen中尝试
接下来,我们要为Clock
组件添加定时器使其每秒自动更新。
在有多个组件的应用中,组件销毁时及时释放其所占用的资源是非常有必要的。
我们想要在Clock
被渲染到DOM后立即建立一个定时器,这在React中是__mount__阶段。
我们还想在当Clock
创建的DOM被销毁时清除该定时器,这在React中是__unmounting__阶段。
我们可以在组件类中定义一些特殊的方法使得组件在mount/unmount时执行一些操作:
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
}
componentWillUnmount() {
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
这些方法被称为"Lifecycle Hooks"
当一个组件被渲染至DOM时执行componentDidMount
,这是一个设置定时器的好时机:
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
注意,我们在这里将定时器的ID保存在了this
中。
this.props
由React自己建立,this.state
有着特殊的意义-当有某些不是用于视觉输出的数据需要存储时,我们可以将其保存在class中。
如果这个数据不是用于render()
中的,就不应该添加在state里。
在componentWillUnmount
阶段,我们将销毁定时器:
componentWillUnmount() {
clearInterval(this.timerID);
}
最后,我们需要完成每秒都执行的tick()
方法。
该方法通过this.setState()
更新组件的本地状态(local state):
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
在CodePen中尝试
现在这个时钟能够开始动起来了。
让我们快速回顾一下发生了什么,以及各方法的调用顺序:
- 当
<Clock />
传递给ReactDOM.render()
时,React调用了<Clock />
的构造函数。 由于Clock
要显示当前时间,其将this.state
初始化为一个包含当前时间的对象。我们将在稍后更新该state。 - React之后调用
Clock
组件的render()
方法,根据该方法在屏幕上绘制UI。 - 当
Clock
输出被插入DOM时,React调用componentDidMount
Life Hook。在该hook中Clock
组件通知浏览器建立一个定时器并每秒调用一次tick()
。 - 浏览器每秒调用一次
tick()
方法。在该方法中,Clock
组件通过调用setState()
安排一次UI更新。setState()
的调用让React收到状态改变的通知,并再次执行render()
方法了解如何在屏幕上绘制UI。这次,render()
方法中的this.state.date
发生了变化,因此渲染输出包含更新了的时间。React根据它来更新DOM。 - 如果
<Clock />
组件从DOM中移除了,React会调用componentWillUnmount
Life Hook,定时器将会被停止。
关于setState()
,我们需要知道三点
不要直接改变state
例如,这种操作无法重新渲染一个组件:
// Wrong
this.state.comment = 'Hello';
应该使用setState()
:
// Correct
this.setState({comment: 'Hello'});
我们唯一能直接给this.state
赋值的地方是在组件的构造函数中。
状态更新可能是异步的
从性能的角度考虑,React可能会在某次单独的更新中批量执行多个setState()
。
由于this.props
和this.state
可能会异步更新,我们不应该依靠他们的值来计算下一步的状态。
例如,以下代码可能无法准确的更新counter:
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
为避免这个问题,可以使用setState()
的另一种形式-接受函数而不是对象。传递给setState的函数将之前的状态作为第一个参数,需增加的属性作为第二个参数:
// Correct
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
我们在这里使用了箭头函数,这么写也是一样的:
// Correct
this.setState(function(prevState, props) {
return {
counter: prevState.counter + props.increment
};
});
状态更新是合并更新
调用setState()
时,React将我们提供的对象合并至当前状态。
例如,我们的state可能包含多个独立的变量:
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
然后我们可以使用单独使用setState()
分别更新它们:
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
调用this.setState({comments})
不会修改this.state中posts的值,而是仅仅修改this.state.comments
的内容。
父组件或子组件都无法知道某组件是否是有状态的,它们也不应该组件是函数式组件还是类组件。
这也是为什么state通常是被本地调用或封装-它只能被拥有并建立它的组件访问,其他组件均不能访问它。
组件可以将state作为props传递给子组件:
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
也可以这么做:
<FormattedDate date={this.state.date} />
FormattedDate
组件将date
作为其属性,但并不知道这个date的来源(是来自Clock
的状态、Clock
的属性或手动设置的):
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
在CodePen中尝试
这通常被称为“自顶向下”或“单向”数据量。任何state都是由某个特定的组件拥有的,并且继承自该状态的任何数据和UI只能影响在他们组件树之下的组件。
将组件树想象为一个属性的瀑布,每个组件的状态(state)就好比是在任意节点中加入的,同样是自上而下的额外水源。
为了表示所有的组件都是真正隔离的,我们可以创建一个App
组件渲染三个<Clock>
:
function App() {
return (
<div>
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
在CodePen中尝试
每个Clock
建立自己的定时器并独立更新。
在React应用中,组件是否有状态被认为是组件的实现细节,其可能会随着时间而改变。我们可以在有状态的组件中使用无状态的组件,反之亦然。
React元素的事件处理与DOM元素很类似,但有一些语法差异:
- React事件采用驼峰命名
- 使用JSX,我们可以直接传递函数而非字符串作为事件处理程序
例如,HTML:
<button onclick="activateLasers()">
Activate Lasers
</button>
这在React中稍有不同:
<button onClick={activateLasers}>
Activate Lasers
</button>
另一个不同之处在于,React中我们无法通过返回false
阻止默认行为。
我们必须显式的调用preventDefault
。例如,在纯HTML中,为防止链接打开新页面的默认行为,我们可以这么写:
<a href="#" onclick="console.log('The link was clicked.'); return false">
Click me
</a>
在React中的写法:
function ActionLink() {
function handleClick(e) {
e.preventDefault();
console.log('The link was clicked.');
}
return (
<a href="#" onClick={handleClick}>
Click me
</a>
);
}
这里的e
是一个事件元素。React根据W3规范定义这一事件,因此我们无需担心浏览器的兼容问题。
在使用React时,在一个DOM创建后,需要为其增加监听事件时,我们通常不采用addEventListener
,而是在元素最初被渲染时提供一个监听器。
当我们采用一个ES6 class定义一个组件时,通常的做法是把事件处理函数作为该类的一个方法。
例如,下面这个Toggle
组件渲染了一个点击切换开关状态的按钮:
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
// This binding is necessary to make `this` work in the callback
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn
}));
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
ReactDOM.render(
<Toggle />,
document.getElementById('root')
);
在CodePen中尝试。
注意JSX回调中的this
。
在JavaScript中,类方法不会默认绑定this。
如果忘记绑定this.handleClick
而将其直接传递给onClick
,当函数被调用时,this
的值是undefined
的。
这并不是React的特有行为,具体可参看这篇文章.
通常来说,如果我们引用一个没有()
的方法,如onClick = {this.handleClick}
,我们应该绑定该方法。
如果不想采用bind
的方式,这里有另外两种选择。
如果你在使用 实验性 的初始化语法,则可以使用属性初始化器完成正确的回调绑定:
class LoggingButton extends React.Component {
// 这个表达式确保this绑在handleClick中
// 注意!!! 这个是实验性语法
handleClick = () => {
console.log('this is:', this);
}
render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
);
}
}
另一种方法是在回调中使用箭头函数:
class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
// 这个表达式确保this绑在handleClick中
return (
<button onClick={(e) => this.handleClick(e)}>
Click me
</button>
);
}
}
使用箭头函数的问题在于这种方式会导致每次渲染LogginButton
时都会创建一个不同的回调函数。
在大多情况下,这么做没什么问题。但是,如果这个回调函数是作为属性被传递给更底层的组件时,该组件可能会被影响,造成一次额外的重新渲染。
为避免这种问题的发生,我们推荐采用在构造函数中手动绑定或使用属性初始化语法。
在React中,我们可以创建多种封装我们需要的行为的组件。然后,我们可以根据应用的状态,只渲染其中的一部分。
React的条件渲染与JS中的条件判断相同。使用JS中的if
或条件操作符创建表示当前状态的元素,然后让React更新UI以匹配它们。
考虑以下两个组件:
function UserGreeting(props) {
return <h1>Welcome back!</h1>;
}
function GuestGreeting(props) {
return <h1>Please sign up.</h1>;
}
我们将创建一个Greeting
组件,其状态取决于用户是否登录:
function Greeting(props) {
const isLoggedIn = props.isLoggedIn;
if (isLoggedIn) {
return <UserGreeting />;
}
return <GuestGreeting />;
}
ReactDOM.render(
// Try changing to isLoggedIn={true}:
<Greeting isLoggedIn={false} />,
document.getElementById('root')
);
在CodePen中尝试。
这里根据属性isLoggedIn
的值显示出不同的问候语。
我们可以使用变量保存元素。这可以帮助我们在输出的其他部分不变的情况下,有条件的渲染组件的一部分。
考虑以下两个表示登录/登出的组件:
function LoginButton(props) {
return (
<button onClick={props.onClick}>
Login
</button>
);
}
function LogoutButton(props) {
return (
<button onClick={props.onClick}>
Logout
</button>
);
}
在下例中,我们会创建一个有状态的组件-LoginControl
。
该组件将根据其现有的状态渲染成<LoginButton />
或<LogoutButton />
,同时也会渲染先前例子中的<Greeting />
:
class LoginControl extends React.Component {
constructor(props) {
super(props);
this.handleLoginClick = this.handleLoginClick.bind(this);
this.handleLogoutClick = this.handleLogoutClick.bind(this);
this.state = {isLoggedIn: false};
}
handleLoginClick() {
this.setState({isLoggedIn: true});
}
handleLogoutClick() {
this.setState({isLoggedIn: false});
}
render() {
const isLoggedIn = this.state.isLoggedIn;
let button = null;
if (isLoggedIn) {
button = <LogoutButton onClick={this.handleLogoutClick} />;
} else {
button = <LoginButton onClick={this.handleLoginClick} />;
}
return (
<div>
<Greeting isLoggedIn={isLoggedIn} />
{button}
</div>
);
}
}
ReactDOM.render(
<LoginControl />,
document.getElementById('root')
);
在CodePen中尝试。
虽然声明一个变量并用if
表达式是一种有条件的渲染组件的好方法,但有时我们可以采用更简洁的表达式。
如下所述,JSX中有几种方法。
我们可以用{}
在JSX中嵌入任意表达式,这当然也包括JS中的&&
。这在某些情况下能非常方便的执行有条件的渲染:
function Mailbox(props) {
const unreadMessages = props.unreadMessages;
return (
<div>
<h1>Hello!</h1>
{unreadMessages.length > 0 &&
<h2>
You have {unreadMessages.length} unread messages.
</h2>
}
</div>
);
}
const messages = ['React', 'Re: React', 'Re:Re: React'];
ReactDOM.render(
<Mailbox unreadMessages={messages} />,
document.getElementById('root')
);
在CodePen中尝试。
这是因为在JS中,true && expression
的结果取决于expression
, false && expression
的结果总是false
。
因此,如果条件是true
,就会输出在&&
之后的元素(也就是<h2>You have ... </h2>
),否则React就会忽略它。
另一种有条件渲染渲染元素的方式是使用JS中的条件表达式condition ? true : false
。
在下例中,我们将使用该表达式渲染一段文本:
render() {
const isLoggedIn = this.state.isLoggedIn;
return (
<div>
The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in.
</div>
);
}
它也可以用于更大的表达式,尽管看起来有些不太直观:
render() {
const isLoggedIn = this.state.isLoggedIn;
return (
<div>
{isLoggedIn ? (
<LogoutButton onClick={this.handleLogoutClick} />
) : (
<LoginButton onClick={this.handleLoginClick} />
)}
</div>
);
}
具体选择哪种方式取决于你和你的团队。
同时也别忘了,当条件表达式变得过于复杂,这通常意味着是时候对组件进行分解了。
在少数情况下,我们也许会需要组件隐藏它自己(即使这个组件是由另一个组件呈现的)。为达到该目的,应在组件的render
方法中返回null
而不是其渲染输出。
在下例中,<WarningBanner />
是否渲染取决于属性值warn
。如果其为false
,该组件不会被渲染:
function WarningBanner(props) {
if (!props.warn) {
return null;
}
return (
<div className="warning">
Warning!
</div>
);
}
class Page extends React.Component {
constructor(props) {
super(props);
this.state = {showWarning: true}
this.handleToggleClick = this.handleToggleClick.bind(this);
}
handleToggleClick() {
this.setState(prevState => ({
showWarning: !prevState.showWarning
}));
}
render() {
return (
<div>
<WarningBanner warn={this.state.showWarning} />
<button onClick={this.handleToggleClick}>
{this.state.showWarning ? 'Hide' : 'Show'}
</button>
</div>
);
}
}
ReactDOM.render(
<Page />,
document.getElementById('root')
);
在CodePen中尝试。
在组件的render
方法中返回null
不会影响组件生命周期方法的触发。比如,componentWillUpdate
和componentDidUpdate
仍将被调用。
首先让我们来回顾一下在JavaScript如何转换列表。
在以下代码段中,我们使用map()
函数,为其传入一个numbers
数组,将数组的值翻倍并打印出来:
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((number) => number * 2);
console.log(doubled);
这段代码在控制台中会打印出[2, 4, 6, 8, 10]
。
在React中,将数组转为元素列表的操作基本类似。
我们可以建立元素集合并用{}
将他们包含在JSX中。
以下代码中,我们使用map()
遍历numbers
数组,对其中的每个元素,返回一个<li>
最后将生成的元素数组赋值给listItems
:
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li>{number}</li>
);
我们使用<ul>
包裹listItems
并将其渲染至DOM:
ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
);
在CodePen中尝试。
这段代码显示了一段1~5的列表。
通常我们会将list渲染至某个组件中。
我们可以将之前的例子重构为一个接受一个数组numbers
并输出一个无序元素列表的组件。
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li>{number}</li>
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
执行这段代码时会收到一个警告-对list的各列表项需要提供一个key。key
是创建元素列表时需要包含的特殊字符串属性。我们将在下一部分讨论它为什么重要。
为numbers.map()
中的列表项分配一个key:
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
keys用于帮助React识别出具体是哪个item发生了变化,它应该被赋给元素数组中的各元素以作为其标识:
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
选择key的最佳实践是使用能够唯一标识各元素的字符串。大多情况下可以使用数据的ID作为key:
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);
数据不一定有id属性时,也可以用item的索引替代:
const todoItems = todos.map((todo, index) =>
<li key={index}>
{todo.text}
</li>
);
在item可以重新排序的情况下,不建议使用index作为key-这样会很慢。
key在数组的上下文中才有意义。
例如,要分解一个listItem
组件,应该将key保留在数组中的<ListItem />
元素上而不是<li>
中。
例:错误的使用方式
function ListItem(props) {
const value = props.value;
return (
// Wrong! There is no need to specify the key here:
<li key={value.toString()}>
{value}
</li>
);
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// Wrong! The key should have been specified here:
<ListItem value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
正确的使用方式:
function ListItem(props) {
// Correct! There is no need to specify the key here:
return <li>{props.value}</li>;
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// Correct! Key should be specified inside the array.
<ListItem key={number.toString()}
value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
在CodePen中尝试。
这里有一个很好的经验法则-map()
调用中的元素需要key
数组中使用的key在兄弟中必须是唯一的。但是,他们不需要 全局唯一 。两个不同的数组中的key可以相同:
function Blog(props) {
const sidebar = (
<ul>
{props.posts.map((post) =>
<li key={post.id}>
{post.title}
</li>
)}
</ul>
);
const content = props.posts.map((post) =>
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
);
return (
<div>
{sidebar}
<hr />
{content}
</div>
);
}
const posts = [
{id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
{id: 2, title: 'Installation', content: 'You can install React from npm.'}
];
ReactDOM.render(
<Blog posts={posts} />,
document.getElementById('root')
);
在CodePen中尝试。
key的是供React使用的,但并不会传递给组件。在组件中若需要同样的值,可以取一个不同的名字并将其作为属性显式传递:
const content = posts.map((post) =>
<Post
key={post.id}
id={post.id}
title={post.title} />
);
在上例中,Post
组件可以读到props.id
,但无法读到props.key
。
上例中声明了一个单独的listItem
变量并将其包含在JSX中:
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
);
return (
<ul>
{listItems}
</ul>
);
JSX允许在{}
中嵌入表达式,因此可以内联map()
的输出:
function NumberList(props) {
const numbers = props.numbers;
return (
<ul>
{numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
)}
</ul>
);
}
在CodePen中尝试。
这么做在一些情况下能使代码更清晰,但有时也会造成麻烦。与JavaScript一样,是否需要为可读性提取变量取决于你。
注意,如果map()
的嵌套层级过多,也许这说明是时候分解组件了。
在React中,HTML表单元素与其他DOM元素略有不同-表单元素具有一些内部状态。例如,以下HTML表单接收一个姓名:
<form>
<label>
Name:
<input type="text" name="name" />
</label>
<input type="submit" value="Submit" />
</form>
用户提交表单时,该表单会执行HTML表单的默认行为。在React中,如果这就是我们想要的效果,那么不需要任何额外操作。
但大多情况下,会需要使用JS函数处理用户输入的数据并执行提交动作。实现这一点的标准方式是使用一种称为“受控组件”的技术。
在HTML中, <input>
,<textarea>
及<select>
等表单元素均有自己的状态,并会根据用户的输入进行更新。
在React中,可变状态通常存放在组件的state属性中,并通过setState()
进行更新。
我们可以结合以上两种方式,采用React的状态作为“单一数据源(single source of truth)”。
渲染表单的React组件在用户输入时进行相应变化,控制组件的显示。采用这种方式,值由React控制的表单元素就叫做“受控组件”。
例如,在上例中,如果想要在用户提交时打印其提交的内容,可将form写成受控组件:
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
属性的赋值方式-value={this.state.value}
,表单元素显示的值是this.state.value
,使用了React的状态作为单一数据源。
由于每次用户输入时都会执行handleChange
更新React的state,元素显示的值也将随着用户的输入而更新。
在受控组件中,状态每次的变化都会与处理函数(handleChange
)相关联,这让我们有机会直接对用户的输入做一些操作,如直接将用户的输入自动转换为大写:
handleChange(event) {
this.setState({value: event.target.value.toUpperCase()});
}
在HTML中,<textarea>
元素通过子节点定义其文本:
<textarea>
Hello there, this is some text in a text area
</textarea>
React中采用value
属性进行替代。这种方式使得使用<textarea>
标签与单行输入的<input>
标签一样方便:
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
在构造函数中初始化,因此textarea在一开始就会有一些文本。
在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是默认被选中的-该option中添加了selected
属性。
React中并不使用selected
属性,而是仍采用为value
赋值的方式,并将该属性置于select根标签中。这种方式在受控组件中更为方便,只需要在一个地方更新该值即可:
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属性,我们通过该属性实现一个受控组件。
当需要处理多个受控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中的计算属性名表达式根据输入更新state中对应key的value:
this.setState({
[name]: value
});
这等同与ES5中的:
var partialState = {};
partialState[name] = value;
this.setState(partialState);
另外,由于setState()
是合并更新(可参看状态与生命周期-"状态更新是合并更新"这一小节),我们只要更新我们需要更新的部分就可以了。
有时大量使用受控组件会很麻烦-对每种数据的更新都要写好相应的处理函数,并通过React组件管理所有输入状态。
在这种情况下,或许我们可以尝试它的替代方案-非受控组件。
有些情况下,多个组件需要多同一个数据的变化做出反应。我们推荐这时可以将该变量提升至这些组件共有的父节点中。
在本部分,我们将创建一个接受给定温度的温度计,其会计算在给定温度下水是否被煮沸。
从组件BoilingBerdict
开始,该组件接受celsisu
作为属性,并打印水是否被煮沸。
function BoilingVerdict(props) {
if (props.celsius >= 100) {
return <p>The water would boil.</p>;
}
return <p>The water would not boil.</p>;
}
下一步,创建Calculator
组件。其渲染一个接受温度输入的<input>
,并在this.state.temperature
中保持该值。
另外,该组件根据当前的输入渲染BoilingVerdict
:
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''};
}
handleChange(e) {
this.setState({temperature: e.target.value});
}
render() {
const temperature = this.state.temperature;
return (
<fieldset>
<legend>Enter temperature in Celsius:</legend>
<input
value={temperature}
onChange={this.handleChange} />
<BoilingVerdict
celsius={parseFloat(temperature)} />
</fieldset>
);
}
}
现在有了一个新需求。除了摄氏度的input外,还需要提供一个华氏度的input,且要求两者保持一致。
我们可以先从Calculator
中提取出TemperatureInput
组件,并新增一个scale
属性,该属性的值为"c"
或"f"
:
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
};
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''};
}
handleChange(e) {
this.setState({temperature: e.target.value});
}
render() {
const temperature = this.state.temperature;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={temperature}
onChange={this.handleChange} />
</fieldset>
);
}
}
现在可以将Calculator
修改为渲染两个不同的温度输入框:
class Calculator extends React.Component {
render() {
return (
<div>
<TemperatureInput scale="c" />
<TemperatureInput scale="f" />
</div>
);
}
}
在CodePen中尝试。
现在我们有两个输入框,但当向其中一个输入框输入数据时,另一个并不会随之改变。而我们的需求是两个输入框保持同步。
另外,在Calculator
中也无法显示BoilingVerdict
。这是因为当前温度的状态已经被隐藏至了TemperatureInput
中,Calculator
无法知道当前温度。
首先,我们将实现两个用于摄氏度与华氏度间转换的函数:
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
这两个函数执行数值间的转换。我们还需要完成另一个函数,该函数接受两个参数-字符串类型的temperature
和上面的两个函数之一。这个函数能够根据输入的温度值(字符串)计算另一个单位的温度值(字符串)。
输出的转换值保留小数点后三位,输入值无效时该函数返回一个空字符串:
function tryConvert(temperature, convert) {
const input = parseFloat(temperature);
if (Number.isNaN(input)) {
return '';
}
const output = convert(input);
const rounded = Math.round(output * 1000) / 1000;
return rounded.toString();
}
例如,tryConvert('abc', toCelsius)
将返回一个空字符串,tryConvert('10.22', toFahrenheit)
将返回'50.396'
。
现在,两个TemperatureInput
组件都在他们自己的本地状态中保留各自的值:
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {temperature: ''};
}
handleChange(e) {
this.setState({temperature: e.target.value});
}
render() {
const temperature = this.state.temperature;
然而,我们希望两个输入框中的值彼此间同步。当我们更新摄氏度输入框的内容时,华氏度输入框应该改变成相对应的值,反之亦然。
React中的状态共享通过将该状态提升至两个组件最近的公共祖先组件的状态完成。这被称之为 "状态提升" 。我们从TemperatureInput
中移除本地状态并将其放置于Calculator
中。
Calculator
拥有这个被共享的状态后,该状态成为了两个温度输入框共享的"单一数据源"。这有助于保持两个输入框内容一致。由于两个TemperatureInput
输入框的属性均来源于同一个父组件Calculator
,因此两个输入框的内容将始终是同步的。
首先。我们将TemperatureInput
组件中的this.state.temperature
替换为this.props.temperature
。现在,假设this.props.temperature
已经存在(接下来会看到,该值由Calculator
传递进来):
render() {
// Before: const temperature = this.state.temperature;
const temperature = this.props.temperature;
我们知道属性是只读的。temperature
作为本地状态时,TemperatureInput
可以通过setState
修改该状态。但当其作为属性被传入时,TemperatureInput
没有修改该属性的能力。
现在,当TemperatureInput
想要修改temperature
时,需调用this.props.onTempatureChange
:
handleChange(e) {
// Before: this.setState({temperature: e.target.value});
this.props.onTemperatureChange(e.target.value);
注意,在我们自定义的组件中,temperature
或onTempatureChange
等属性名均不具有特殊含义。我们可以任意命名,如value
和onChange
等。
onTempatureChange
属性和temperature
一样由父组件Calculator
传入。该方法通过修改本地状态来处理变更,因此会采用新的值同时更新两个输入框。接下来我们马上将看到Calculator
的具体实现。
在修改Calculator
前,让我们回顾一下对TemperatureInput
组件的修改。我们从组件中移除了其本地状态,将this.state.temperature
转成了this.props.temperature
。之前通过this.setState()
执行变更,现在转为通过由Calculator
提供的this.props.onTempatureChange
实现:
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.props.onTemperatureChange(e.target.value);
}
render() {
const temperature = this.props.temperature;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={temperature}
onChange={this.handleChange} />
</fieldset>
);
}
}
现在再来看Calculator
组件。
我们将当前输入框的temperature
及该输入框的计量单位
作为Calculator
的本地状态,这个状态从输入框中提升而来,并作为两个输入框的"单一数据源"。
例如,当我们在Celsius
输入框中输入 37 时,Calculator
的state是:
{
temperature: '37',
scale: 'c'
}
随后如果我们在Fahrenheit
中输入 212 ,Calculator
中的state将变为:
{
temperature: '212',
scale: 'f'
}
我们可以同时存储两个输入框中的值,但事实上并没有这个必要。存储最近被更新的输入框的值及该输入框所代表的单位即可。我们可以通过这两者计算出另一个输入框中应显示的内容。
之所以说输入框同步,是因为他们的数据值均从同一个数据源计算而来:
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
this.state = {temperature: '', scale: 'c'};
}
handleCelsiusChange(temperature) {
this.setState({scale: 'c', temperature});
}
handleFahrenheitChange(temperature) {
this.setState({scale: 'f', temperature});
}
render() {
const scale = this.state.scale;
const temperature = this.state.temperature;
const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
return (
<div>
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={this.handleCelsiusChange} />
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={this.handleFahrenheitChange} />
<BoilingVerdict
celsius={parseFloat(celsius)} />
</div>
);
}
}
在CodePen中尝试。
现在,无论我们在输入框中输入什么,Calculator
中的this.state.temperature
和this.state.scale
都会得到相应的更新。只要其中一个输入框的值被用户更新,另一个也会被重新计算。
让我们回顾一下,当编辑一个输入框时,发生了什么:
- React调用
<input>
中被指定的onChange
方法。在本例中,该方法为TemperatureInput
中的handleChange
。 TemperatureInput
组件中的handleChange
方法调用this.props.onTempatureChange
。该组件的属性(包括onTempatureChange
)由其父组件Calculator
提供- 在其被渲染前,
Calculator
为单位是摄氏度的TemperatureInput
指定的onTempatureChange
的方法是handleCelsiusChange
,为华氏度指定的是handleFahrenheitChange
。因此我们在不同的输入框输入内容时会调用Calculator
中相应的handlexxChange
方法。 - 在这些方法中,
Calculator
组件根据输入框中由用户更新的温度值及该输入框的单位,调用setState
更新自己的状态,从而令React对其重新绘制。 - React调用
Calculator
组件的render
方法绘制组件。两个输入框中的值都会根据用户输入的温度和被用户输入的那个输入框的单位重新计算。在这一步,进行了温度的转换。 - React调用各
TemperatureInput
的render
方法及其由Calculator
赋给的新属性值,绘制出两个输入框。 - React DOM更新DOM。我们刚刚编辑的输入框接受其当前值,另一个输入框更新为转换后的温度。
React应用中任何数据的改变都应遵从 单一数据源 原则。
通常情况下,状态应被优先加入到渲染时依赖该值的组件中。但当有其他组件也依赖于该状态时,我们可以将其提升至组件们的最近公共祖先组件中,而不是尝试同步不同组件间的状态。我们应该遵守数据流的从上至下的原则。
相对双向绑定来说,状态提升需要编写更多无意义的样板代码,但这么做的好处在于能够更方便的查找与隔离bugs-因为任何状态都只能由某一个组件对其进行修改,这能大大的减少查找bug产生的范围。另外,我们可以轻松的自定义任何逻辑对用户的输入进行预处理(如转换或禁止输入等)。
当某一个数据可以通过属性或状态得到,那么该数据也许不适合作为状态存在。例如我们先前的例子,我们并没有同时保存celsiusValue
和fahrenheitValue
两个值,而是保存了用户最近一次输入的数值和用户输入的输入框单位。另一个输入框的内容可以通过render
方法计算出来。 这么做让我们能够在无论是否使用四舍五入等估算方法,都不会丢失用户输入的精度。
当我们看到UI出现不符合预期的错误时,我们可以通过React开发者工具进行排查,揪出负责更新该状态的组件。这对找出bug的源头非常有帮助:
React拥有强大的组合模型,我们建议使用组合而非继承来重用组件间的代码。
在本部分,我们将展示一些刚接触React的开发者在面对某些情况时可能使用继承的例子,并用组合的方式解决他们。
一些组件在开始时不能确定是否有子组件,尤其是对Sidebar
和Dialog
等这类用于表示box
的组件而言特别常见。
对于这类组件,我们建议使用特定的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中尝试。
在JSX标签<FancyBorder>
中包含的所有东西都会作为children
属性传递进去。由于FancyBorder
在<div>
中渲染{props.children}
,传递给FancyBorder
的元素直接被显示出来。
还有一类比较少见的情况,有时我们可能在一个组件中需要多个占位符。这时我们不必要强制使用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中尝试。
React元素<Contacts />
和<Chat />
就是对象,我们可以像传递其他数据一样,将他们作为属性进行传递。
有时我们会遇到某个组件被另一个组件包含的情况,如WelcomeDialog
是一种特殊的Dialog
。
在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}!`);
}
}
在现有的React组件实践中,我们暂时还没有发现任何一种使用继承能够优于组合方式的情况。
使用属性和组合已经能为我们提供明确、安全的方法定制我们组件的外观和行为。 记住,组件可以接受任何属性,包括原始值,React元素或函数。
想要在组件中复用非UI功能的代码,我们建议将其提取至单独的JS模块中。组件可以导入该模块,使用其中的函数、对象或类,而不需要去扩展它。
从我们的角度来看,React是使用JavaScript构建大型、高效Web应用的首选,这在Facebook和Instagram中已经很好的证明了。
React最棒的部分之一在于-当我们构建我们的产品时,它会影响我们对产品的思考。在本部分,我们将引导您完成一个使用React构建某应用(一个可搜索的产品列表)的思考过程。
假设我们已经有一个JSON API和一个设计师的初稿,如图所示: JSON API返回的数据为:
[
{category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
{category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
{category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
{category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
{category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
{category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];
我们首先会做的事是为视觉稿中的每个组件、子组件画框,并给他们命名。如果我们是和设计师一起工作的,他们可能已经完成了这部分内容。和他们交流一下,约定一下命名规范。他们在PS上给图层的命名可能最终成为我们React组件的名称。
但我们应如何拆分组件?把这当做拆分一个新方法或新对象吧。一个技巧是 单一职责原则 ,也就是说,理想情况下,一个组件只做一件事。若某个组件正变得越来越复杂,我们有必要对其进行进一步的拆分。
由于常常需要向用户展示JSON数据模型,若模型建立的正确,UI(以及组件结构)能够得到很好的映射。这是因为UI和数据模型往往拥有相同的信息结构,也就是说,将UI拆分成组件的工作通常比较琐碎。只需将其拆分成对应数据模型的片段即可。
可以看到,在我们这个简单的应用中有5个组件,我们已经对每个组件代码的数据进行了标注。
FilterableProducTable
(橘色):包含整个实例。SearchBar
(蓝色):接收所有的用户输入。ProductTable
(绿色):基于用户的输入显示并过滤数据集合。ProductCategoryRow
(宝石绿):显示每个类别的的标题。ProductRow
(红色):按行显示每个产品的数据。
以ProductCategoryRow
为例子,我们可以发现包含"姓名"和"价格"的表头并不是独立的组件。这属于个人选择,也可以有别的方案。在本例中,将其作为ProductTable
的一部分来处理,是因为其是数据集合渲染的一部分(这部分属于ProductTable
的职责)。然而,若表头很复杂(如需要为其添加排序功能等),则将其拆分成一个ProductTableHeader
组件是很有必要的。
现在我们在视觉稿中标识出了我们的组件,现在让我们对他们进行层级排列。出现在A组件内部的组件B,就是A的子组件:
FilterableProducTable
SearchBar
ProductTable
ProductCategoryRow
ProductRow
在CodePen中查看。
有了组件的层次结构,是时候实现我们的APP了。最简单的方式是先建立一个“接受数据模型并渲染UI,但不存在交互”的版本。最好解耦这些处理-建立一个静态版本不需要思考太多,但添加交互往往需要考虑很多情况。
建立一个渲染数据模型的静态APP,需要构建复用其他组件并通过属性传递数据的组件。属性是一个从父组件向子组件传递数据的方法。如果您熟悉状态(state)的概念, 不要在这个静态版本的APP中使用state。 state只用于交互,也就是那些会随时变动的数据。由于这是个静态APP,我们不需要使用state。
我们可以自顶向下或自底向上构建应用。在构建简单的应用时,通常自顶向下更容易。在构建大型应用时,自底向上能够更方便与编写测试用例。
在这步结束时,我们已经有了一个可重用的,渲染数据模型的组件库。由于是个静态应用,这些组件只用render()
方法。层级结构顶层的组件(FilterableProducTable
)将接收数据模型作为一个属性(props)。当我们修改数据模型并再次调用ReactDOM.render()
时,UI将同步更新。由于过程比较简单,观测UI的更新及相关数据的变化是非常方便的。React的 单向数据流 (也称为单向绑定)能够在开发过程中保证模块化和高性能。
为了让UI变得可交互,我们需要能更改底层的数据模型。React通过state使这一过程变得容易。
为了正确的构架应用,我们首先要考虑的是应用需要的可变状态的最小集合。这里的关键是DRY(Don't Repeat Yourself)原则。找出应用所需的最小的状态的集合,确保该状态集合可按需计算出应用的各状态。例如,若我们正在建立一个TODO列表,那么我们只要保留TODO元素数组即可,不需要用一个单独的state用于存储数组的长度(该数字可由数组得出)。
想想我们例子中的所有数据,我们有:
- 产品的原始列表
- 用户输入的搜索文本
- 复选框的值
- 过滤后的产品列表
让我们过一遍这些数据,确认那些属于state。我们可以通过三个问题进行判断:
- 其是否由父组件通过props传递-若是,其可能不是state
- 其是否是固定不变的-若是,其可能不是state
- 该值可通过其他state或props计算出来吗-若是,其可能不是state
产品的原始列表作为props传递,因此其不是state。用户输入的搜索文本和复选框的值看起来是state(他们会发生变化,且无法通过其他props/state计算得出)。最后,过滤后的产品列表不属于state-其可以通过产品的原始列表+用户输入的搜索文本+复选框的值计算得出。
所以,根据我们得到的结果,最终的state是:
- 用户输入的搜索文本
- 复选框的值
在CodePen中查看。
我们在上一步确认了应用的最小state集合。接下来,要确定哪些组件是可变的。或者说,确定这些state的owner。
记住:React是自上而下的单向数据流层级。这也许会导致无法立即确定state应放在哪个组件比较合适。我们可以按照以下步骤进行确定:
对于应用中的每个state:
- 确定每个基于该state渲染的组件
- 找到公共的父组件(及所有需要该state组件的公共祖先组件)
- 该state应置于公共父组件或更高层级的组件中
- 若无法找到合适的放置state的位置,可创建一个新组件存储该state,并将这个新组件添加至公共父组件的上层
将这个策略应用于我们的例子中: ProductTable
需要机遇state过滤产品列表。SearchBar
需要展示搜索文本和选中状态- 公共父组件为
FilterableProducTable
- 从概念上来说,将过滤文本和选中的值置于
FilterableProducTable
是合适的
综上,我们将state置于FilterableProducTable
中。
首先,添加一个实例属性this.state = {filterText: '', inStockOnly: false}
到FilterableProducTable
的构造函数中,用于初始化程序的状态。
然后,将filterText
和isStockOnly
作为props传递给ProductTable
和SearchBar
。
最后,使用这些props过滤ProductTable
中的行,并设置表单字段中SearchBar
的值。
我们可以看一下我们应用的行为:在filterText
中设置"ball"
并刷新应用。我们可以看到数据被正确的更新了。
在CodePen中查看。
现在,我们已经构建了一个能够正确渲染、数据流从上至下的应用。是时候支持另一种反向的数据流方式了-层次结构中底层的form组件须有更新FilterableProducTable
中的state。
React采用了明确的数据流向,使我们能够更轻松的理解程序的工作过程。但这也使得其相对于传统的双向数据绑定而言,在有些情况下需要多敲一些代码。
如果我们尝试在当前版本中修改复选框的内容,我们会发现React并未对其作出响应-这是有意为之的,我们已经设置了input
中value
的props由FilterableProducTable
的state
传递而来,使其始终保持一致。
那么我们想要的是什么呢?我们想保证无论用户何时修改表格中的内容,state都会及时更新并反映出这些变化。由于组件应该只能够更新他们自己的state,FilterableProducTable
将传递回调函数给SearchBar
,这些回调函数在需要更新state时触发。我们可以将其绑定在onChange事件上接受通知。由FilterableProducTable
传递的回调函数将调用setState()
,从而应用程序会进行更新。
尽管这听起来很复杂,但其实只需少量的代码就能够实现。同时这能够清晰的展示应用中的数据流向。
希望这能够给你一些在关于使用React构建组件和应用方法的想法。这种方法也许确实会让你比平时多写一些代码,但请记住,代码的可读性、模块性远比少写几行代码来的重要。当创建一个大型组件库时,您会感激这些结构清晰、模块化且可重用的代码:-)