React基础概念学习笔记
绝大部分摘抄自 https://react.docschina.org/ ,有部分删改,例子详情可官网查看
文章目录
JSX学习
JSX概念
JSX,是一个 JavaScript 的语法扩展。在 React 中配合使用 JSX,JSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式。JSX 可能会使人联想到模版语言,但它具有 JavaScript 的全部功能。
const element = <h1>Hello, world!</h1>;
React 认为渲染逻辑本质上与其他 UI 逻辑内在耦合,比如,在 UI 中需要绑定处理事件、在某些时刻状态发生变化时需要通知到 UI,以及需要在 UI 中展示准备好的数据。
React 并没有采用将标记与逻辑进行分离到不同文件这种人为地分离方式,而是通过将二者共同存放在称之为“组件”的松散耦合单元之中,来实现关注点分离。
JSX 中嵌入表达式
声明了一个名为 name
的变量,然后在 JSX 中使用它,并将它包裹在大括号中:
const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;
ReactDOM.render(
element,
document.getElementById('root')
);
在 JSX 语法中,可以在大括号内放置任何有效的JS表达式。例如,2 + 2
,user.firstName
或 formatName(user)
都是有效的 JavaScript 表达式。
JSX也是表达式
在编译之后,JSX 表达式会被转为普通 JavaScript 函数调用,并且对其取值后得到 JavaScript 对象。
也就是说,你可以在 if
语句和 for
循环的代码块中使用 JSX,将 JSX 赋值给变量,把 JSX 当作参数传入,以及从函数中返回 JSX:
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}
JSX特定属性
可以通过使用引号,来将属性值指定为字符串字面量:
const element = <div tabIndex="0"></div>;
也可以使用大括号,来在属性值中插入一个 JavaScript 表达式:
const element = <img src={user.avatarUrl}></img>;
在属性中嵌入 JavaScript 表达式时,不要在大括号外面加上引号。应该仅使用引号(对于字符串值)或大括号(对于表达式)中的一个,对于同一属性不能同时使用这两种符号。
因为 JSX 语法上更接近 JavaScript 而不是 HTML,所以 React DOM 使用 camelCase
(小驼峰命名)来定义属性的名称,而不使用 HTML 属性名称的命名约定。例如,JSX 里的 class
变成了 className。
使用 JSX 指定子元素
假如一个标签里面没有内容,可以使用 />
来闭合标签,就像 XML 语法一样:
const element = <img src={user.avatarUrl} />;
JSX 标签里能够包含很多子元素:
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
JSX防止注入攻击
可以安全地在 JSX 当中插入用户输入内容:
const title = response.potentiallyMaliciousInput;
// 直接使用是安全的:
const element = <h1>{title}</h1>;
React DOM 在渲染所有输入内容之前,默认会进行转义,故而不会注入非自己明确编写的内容,防止XSS攻击。
JSX 表示对象
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 以及保持随时更新。
元素渲染
元素描述了在屏幕上想看到的内容。
const element = <h1>Hello, world</h1>;
与浏览器的 DOM 元素不同,React 元素是创建开销极小的普通对象。React DOM 会负责更新 DOM 来与 React 元素保持一致。
注意:元素与组件 概念不同!
将一个元素渲染为 DOM
HTML 文件某处有一个 <div>
:
<div id="root"></div>
我们将其称为“根” DOM 节点,因为该节点内的所有内容都将由 React DOM 管理。
仅使用 React 构建的应用通常只有单一的根 DOM 节点。如果你在将 React 集成进一个已有应用,那么你可以在应用中包含任意多的独立根 DOM 节点。
想要将一个 React 元素渲染到根 DOM 节点中,只需把它们一起传入 ReactDOM.render()
:
const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));
更新已渲染的元素
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);
setInterval()
回调函数,每秒都调用 ReactDOM.render()
。
注意:
在实践中,大多数 React 应用只会调用一次
ReactDOM.render()
。React 只更新它需要更新的部分React DOM 会将元素和它的子元素与它们之前的状态进行比较,并只会进行必要的更新来使 DOM 达到预期的状态。
组件 & Props
组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。
函数组件与 class 组件
定义组件最简单的方式就是编写 JavaScript 函数:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
该函数是一个有效的 React 组件,因为它接收唯一带有数据的 “props”(代表属性)对象与并返回一个 React 元素。这类组件被称为“函数组件”,因为它本质上就是 JavaScript 函数。
同时还可以使用 ES6的class来定义组件:
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
上述两个组件在 React 里是等效的。
渲染组件
React 元素也可以是用户自定义的组件:
const element = <Welcome name="Sara" />;
当 React 元素为用户自定义组件时,它会将 JSX 所接收的属性(attributes)转换为单个对象传递给组件,这个对象被称之为 “props”。
注意: 组件名称必须以大写字母开头。React 会将以小写字母开头的组件视为原生 DOM 标签。例如, 代表 HTML 的div
标签,而 则代表一个组件,并且需在作用域内使用 Welcome
。
组合组件
组件可以在其输出中引用其他组件。这就可以让我们用同一组件来抽象出任意层次的细节。按钮,表单,对话框,甚至整个屏幕的内容:在 React 应用程序中,这些通常都会以组件的形式表示。
例如,我们可以创建一个可以多次渲染 Welcome
组件的 App
组件:
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')
);
通常来说,每个新的 React 应用程序的顶层组件都是 App
组件。但是,如果你将 React 集成到现有的应用程序中,可能需要使用像 Button
这样的小组件,并自下而上地将这类组件逐步应用到视图层的每一处。
提取组件
组件拆分为更小的组件。
例如,参考如下 Comment
组件:
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>
);
}
该组件用于描述一个社交媒体网站上的评论功能,它接收 author
(对象),text
(字符串)以及 date
(日期)作为 props。
该组件由于嵌套的关系,变得难以维护,且很难复用它的各个部分。因此,让我们从中提取一些组件出来。
1、首先,我们将提取 Avatar
组件:
function Avatar(props) {
return (
<img className="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
);
}
Avatar
不需知道它在 Comment
组件内部是如何渲染的。因此,我们给它的 props 起了一个更通用的名字:user
,而不是 author
。
我们建议从组件自身的角度命名 props,而不是依赖于调用组件的上下文命名。
2、我们现在针对 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>
);
}
3、接下来,提取 UserInfo
组件,该组件在用户名旁渲染 Avatar
组件:
function UserInfo(props) {
return (
<div className="UserInfo">
<Avatar user={props.user} />
<div className="UserInfo-name">
{props.user.name}
</div>
</div>
);
}
4、进一步简化 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>
);
}
最初看上去,提取组件可能是一件繁重的工作,但是,在大型应用中,构建可复用组件库是完全值得的。根据经验来看,如果 UI 中有一部分被多次使用(Button
,Panel
,Avatar
),或者组件本身就足够复杂(App
,FeedStory
,Comment
),那么它就是一个可复用组件的候选项。
Props 的只读性
React 非常灵活,但它也有一个严格的规则:
所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
当然,应用程序的 UI 是动态的,并会伴随着时间的推移而变化。在下一章State&生命周期中,我们将介绍一种新的概念,称之为 “state”。在不违反上述规则的情况下,state 允许 React 组件随用户操作、网络响应或者其他变化而动态更改输出内容。
State & 生命周期
https://react.docschina.org/docs/react-component.html 组件API
State 与 props 类似,但是 state 是私有的,并且完全受控于当前组件。
将函数组件转换成 class 组件
通过以下五步将 Clock
的函数组件转成 class 组件:
- 创建一个同名的 ES6 class,并且继承于
React.Component
。 - 添加一个空的
render()
方法。 - 将函数体移动到
render()
方法之中。 - 在
render()
方法中使用this.props
替换props
。 - 删除剩余的空函数声明。
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
现在 Clock
组件被定义为 class,而不是函数。
每次组件更新时 render
方法都会被调用,但只要在相同的 DOM 节点中渲染 ,就仅有一个 Clock
组件的 class 实例被创建使用。这就使得我们可以使用如 state 或生命周期方法等很多其他特性。
向 class 组件中添加局部的 state
1、将render()方法中this.props.属性(如date)替换成this.state.属性(如date)
2、添加一个class构造函数,然后在该函数中为this.state赋初值
通过以下方式将 props
传递到父类的构造函数中:
constructor(props) {
super(props);
this.state = {date: new Date()};
}
class 组件应该始终使用 props
参数来调用父类的构造函数。
3、移除元素中的 date
属性:
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
具体例子见 https://react.docschina.org/docs/state-and-lifecycle.html
将生命周期方法添加到 Class 中
在具有许多组件的应用程序中,当组件被销毁时释放所占用的资源是非常重要的。
当 Clock
组件第一次被渲染到 DOM 中的时候,就为其设置一个计时器 。这在 React 中被称为“挂载(mount)”。
同时,当 DOM 中 Clock
组件被删除的时候,应该清除计时器。这在 React 中被称为“卸载(unmount)”。
我们可以为 class 组件声明一些特殊的方法,当组件挂载或卸载时就会去执行这些方法。
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>
);
}
}
这些方法叫做“生命周期方法”。
componentDidMount()
方法会在组件已经被渲染到 DOM 中后运行,所以,最好在这里设置计时器:
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
接下来把计时器的 ID 保存在 this
之中(this.timerID
)。
尽管 this.props
和 this.state
是 React 本身设置的,且都拥有特殊的含义,但是其实你可以向 class 中随意添加不参与数据流(比如计时器 ID)的额外字段。
我们会在 componentWillUnmount()
生命周期方法中清除计时器:
componentWillUnmount() {
clearInterval(this.timerID);
}
最后,我们会实现一个叫 tick()
的方法,Clock
组件每秒都会调用它。
使用 this.setState()
来时刻更新组件 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')
);
快速概括一下发生了什么和这些方法的调用顺序:
-
当
Clock
被传给ReactDOM.render()
的时候,React 会调用Clock
组件的构造函数。 因为Clock
需要显示当前的时间,所以它会用一个包含当前时间的对象来初始化this.state
。我们会在之后更新 state。 -
之后 React 会调用组件的
render()
方法。这就是 React 确定该在页面上展示什么的方式。然后 React 更新 DOM 来匹配Clock
渲染的输出。 -
当
Clock
的输出被插入到 DOM 中后, React 就会调用ComponentDidMount()
生命周期方法。在这个方法中,Clock
组件向浏览器请求设置一个计时器来每秒调用一次组件的tick()
方法。 -
浏览器每秒都会调用一次
tick()
方法。 在这方法之中,Clock
组件会通过调用setState()
来计划进行一次 UI 更新。得益于setState()
的调用,React 能够知道 state 已经改变了,然后会重新调用render()
方法来确定页面上该显示什么。这一次,render()
方法中的this.state.date
就不一样了,如此以来就会渲染输出更新过的时间。React 也会相应的更新 DOM。 -
一旦
Clock
组件从 DOM 中被移除,React 就会调用componentWillUnmount()
生命周期方法,这样计时器就停止了。正确使用State
不要直接修改 State
例如,此代码不会重新渲染组件:
// Wrong this.state.comment = 'Hello';
而是应该使用
setState()
:// Correct this.setState({comment: 'Hello'});
构造函数是唯一可以给
this.state
赋值的地方:State 的更新可能是异步的
出于性能考虑,React 可能会把多个
setState()
调用合并成一个调用。因为
this.props
和this.state
可能会异步更新,所以你不要依赖他们的值来更新下一个状态。例如,此代码可能会无法更新计数器:
// Wrong this.setState({ counter: this.state.counter + this.props.increment, });
要解决这个问题,可以让
setState()
接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:// Correct this.setState((state, props) => ({ counter: state.counter + props.increment }));
上面使用了箭头函数,不过使用普通的函数也同样可以:
// Correct this.setState(function(state, props) { return { counter: state.counter + props.increment }; });
State 的更新会被合并
当你调用
setState()
的时候,React 会把你提供的对象合并到当前的 state。例如,你的 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
。
数据是向下流动的
不管是父组件或是子组件都无法知道某个组件是有状态的还是无状态的,并且它们也并不关心它是函数组件还是 class 组件。
这就是为什么称 state 为局部的或是封装的的原因。除了拥有并设置了它的组件,其他组件都无法访问。
组件可以选择把它的 state 作为 props 向下传递到它的子组件中, 这对于自定义组件同样适用 。
这通常会被叫做“自上而下”或是“单向”的数据流。任何的 state 总是所属于特定的组件,而且从该 state 派生的任何数据或 UI 只能影响树中“低于”它们的组件。
如果你把一个以组件构成的树想象成一个 props 的数据瀑布的话,那么每一个组件的 state 就像是在任意一点上给瀑布增加额外的水源,但是它只能向下流动。
事件处理
react 元素的事件处理和 DOM 元素的很相似,但是有一点语法上的不同:
- React 事件的命名采用小驼峰式(camelCase),而不是纯小写。
- 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。
在 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>
);
}
使用 React 时,你一般不需要使用 addEventListener
为已创建的 DOM 元素添加监听器。事实上,你只需要在该元素初始渲染的时候添加监听器即可。
当你使用 ES6 class 语法定义一个组件的时候,通常的做法是将事件处理函数声明为 class 中的方法。例如,下面的 Toggle
组件会渲染一个让用户切换开关状态的按钮:
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
// 为了在回调中使用 `this`,这个绑定是必不可少的
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(state => ({
isToggleOn: !state.isToggleOn
}));
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
ReactDOM.render(
<Toggle />,
document.getElementById('root')
);
必须谨慎对待 JSX 回调函数中的 this
,在 JavaScript 中,class 的方法默认不会绑定 this
。如果你忘记绑定 this.handleClick
并把它传入了 onClick
,当你调用这个函数的时候 this
的值为 undefined
。
这并不是 React 特有的行为;这其实与 JavaScript 函数工作原理有关。通常情况下,如果你没有在方法后面添加 ()
,例如 onClick={this.handleClick}
,你应该为这个方法绑定 this
。( 如果觉得使用 bind
很麻烦,这里有两种方式可以解决 :或者用class fields语法,或者在回调用箭头函数)
class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
// 此语法确保 `handleClick` 内的 `this` 已被绑定。
return (
<button onClick={(e) => this.handleClick(e)}>
Click me
</button>
);
}
}
此语法问题在于每次渲染 LoggingButton
时都会创建不同的回调函数。在大多数情况下,这没什么问题,但如果该回调函数作为 prop 传入子组件时,这些组件可能会进行额外的重新渲染。通常建议在构造器中绑定或使用 class fields 语法来避免这类性能问题。
向事件处理程序传递参数
在循环中,通常我们会为事件处理函数传递额外的参数。例如,若 id
是你要删除那一行的 ID,以下两种方式都可以向事件处理函数传递参数:
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
上述两种方式是等价的,分别通过箭头函数和 Function.prototype.bind
来实现。
在这两种情况下,React 的事件对象 e
会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind
的方式,事件对象以及更多的参数将会被隐式的进行传递。
条件渲染
在 React 中,你可以创建不同的组件来封装各种你需要的行为。然后,依据应用的不同状态,你可以只渲染对应状态下的部分内容。
React 中的条件渲染和 JavaScript 中的一样,使用 JavaScript 运算符 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')
);
元素变量
可以使用变量来储存元素。 它可以帮助你有条件地渲染组件的一部分,而其他的渲染部分并不会因此而改变。
观察这两个组件,它们分别代表了注销和登录按钮:
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函数更新
render() {
const isLoggedIn = this.state.isLoggedIn;
let button;
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')
);
与运算符 &&
通过花括号包裹代码,你可以在JSX中嵌入任何表达式。这也包括 JavaScript 中的逻辑与 (&&) 运算符。它可以很方便地进行元素的条件渲染。
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')
);
之所以能这样做,是因为在 JavaScript 中,true && expression
总是会返回 expression
, 而 false && expression
总是会返回 false
。
因此,如果条件是 true
,&&
右侧的元素就会被渲染,如果是 false
,React 会忽略并跳过它。
三目运算符
另一种内联条件渲染的方法是使用 JavaScript 中的三目运算符 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>
);
}
就像在 JavaScript 中一样,你可以根据团队的习惯来选择可读性更高的代码风格。需要注意的是,如果条件变得过于复杂,那你应该考虑如何提取组件。
阻止组件渲染
在极少数情况下,你可能希望能隐藏组件,即使它已经被其他组件渲染。若要完成此操作,你可以让 render
方法直接返回 null
,而不进行任何渲染。
下面的示例中,`` 会根据 prop 中 warn
的值来进行条件渲染。如果 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(state => ({
showWarning: !state.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')
);
在组件的 render
方法中返回 null
并不会影响组件的生命周期。例如,上面这个示例中,componentDidUpdate
依然会被调用。
列表 & Key
渲染多个组件
你可以通过使用 {}
在 JSX 内构建一个元素集合。
下面,我们使用 Javascript 中的map() 方法来遍历 numbers
数组。将数组中的每个元素变成 li
标签,最后我们将得到的数组赋值给 listItems
:
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li>{number}</li>
);
我们把整个 listItems
插入到 ul
元素中,然后渲染进DOM:
ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
);
基础列表组件
通常你需要在一个组件中渲染列表。
我们可以把前面的例子重构成一个组件,这个组件接收 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')
);
当我们运行这段代码,将会看到一个警告 a key should be provided for list items
,意思是当你创建一个元素时,必须包括一个特殊的 key
属性。
让我们来给每个列表元素分配一个 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')
);
key
key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。
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 的时候,万不得已你可以使用元素索引 index 作为 key:
const todoItems = todos.map((todo, index) =>
// Only do this if items have no stable IDs
<li key={index}>
{todo.text}
</li>
);
如果列表项目的顺序可能会变化,不建议使用索引来用作 key 值,因为这样做会导致性能变差,还可能引起组件状态的问题。
用 key 提取组件
元素的 key 只有放在就近的数组上下文中才有意义。
比方说,如果你提取出一个 ListItem
组件,你应该把 key 保留在数组中的这个 <ListItem />
元素上,而不是放在 ListItem
组件中的 <li>
元素上。
一个好的经验法则是:在 map()
方法中的元素需要设置 key 属性。
key 只是在兄弟节点之间必须唯一
数组元素中使用的 key 在其兄弟节点之间应该是独一无二的。然而,它们不需要是全局唯一的。当我们生成两个不同的数组时,我们可以使用相同的 key 值
key 会传递信息给 React ,但不会传递给你的组件。如果你的组件中需要使用 key
属性的值,请用其他属性名显式传递这个值:
const content = posts.map((post) =>
<Post
key={post.id}
id={post.id}
title={post.title} />
);
上面例子中,Post
组件可以读出 props.id
,但是不能读出 props.key
。
在上面的例子中,我们声明了一个单独的 listItems
变量并将其包含在 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>
);
}
这么做有时可以使你的代码更清晰,但有时这种风格也会被滥用。就像在 JavaScript 中一样,何时需要为了可读性提取出一个变量,这完全取决于你。但请记住,如果一个 map()
嵌套了太多层级,那可能就是提取组件的好时机。
表单
受控组件
在 HTML 中,表单元素(如<input
、 textarea
和 select
)之类的表单元素通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用setState()
来更新。
我们可以把两者结合起来,使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。
例如,如果我们想让前一个示例在提交时打印出名称,我们可以将表单写为受控组件:
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('提交的名字: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
名字:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="提交" />
</form>
);
}
}
由于在表单元素上设置了 value
属性,因此显示的值将始终为 this.state.value
,这使得 React 的 state 成为唯一数据源。由于 handlechange
在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新。
对于受控组件来说,每个 state 突变都有一个相关的处理函数。这使得修改或验证用户输入变得简单。例如,如果我们要强制要求所有名称都用大写字母书写,我们可以将 handlechange
改写为:
handleChange(event) {
this.setState({value: event.target.value.toUpperCase()});
}
textarea 标签
在 HTML 中, textarea
元素通过其子元素定义其文本:
<textarea>
你好, 这是在 text area 里的文本
</textarea>
而在 React 中,使用 `value` 属性代替。这样,可以使得使用
的表单和使用单行 input 的表单非常类似:
class EssayForm extends React.Component {
constructor(props) {
super(props);
this.state = {
value: '请撰写一篇关于你喜欢的 DOM 元素的文章.'
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('提交的文章: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
文章:
<textarea value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="提交" />
</form>
);
}
}
请注意,this.state.value
初始化于构造函数中,因此文本区域默认有初值。
select 标签
React 并不会使用 selected
属性,而是在根 select
标签上使用 value
属性。这在受控组件中更便捷,因为您只需要在根标签中更新它。
例子参照 https://react.docschina.org/docs/forms.html
总的来说,这使得 <input type="text"
, <textarea>
和 <select>
之类的标签都非常相似—它们都接受一个 value
属性,你可以使用它来实现受控组件。
注意
你可以将数组传递到
value
属性中,以支持在select
标签中选择多个选项:<select multiple={true} value={['B', 'C']}>
文件 input 标签
在 HTML 中,<input type=“file”>
允许用户从存储设备中选择一个或多个文件,将其上传到服务器,或通过使用 JavaScript 的进行控制。
<input type="file" />
因为它的 value 只读,所以它是 React 中的一个非受控组件。
处理多个输入
当需要处理多个 input
元素时,我们可以给每个元素添加 name
属性,并让处理函数根据 event.target.name
的值选择要执行的操作。
受控输入空值
在 受控组件上指定 value 的 prop 可以防止用户更改输入。如果指定了 value
,但输入仍可编辑,则可能是意外地将value
设置为 undefined
或 null
。
下面的代码演示了这一点。(输入最初被锁定,但在短时间延迟后变为可编辑。)
ReactDOM.render(<input value="hi" />, mountNode);
setTimeout(function() {
ReactDOM.render(<input value={null} />, mountNode);
}, 1000);
另,非受控组件可见: https://react.docschina.org/docs/uncontrolled-components.html
状态提升
通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。让我们看看它是如何运作的。
在本节中,我们将创建一个用于计算水在给定温度下是否会沸腾的温度计算器。
(具体例子见: https://react.docschina.org/docs/lifting-state-up.html )
组合vs继承
React 有十分强大的组合模式。推荐使用组合而非继承来实现组件间的代码重用。
包含关系
有些组件无法提前知晓它们子组件的具体内容。在 Sidebar
(侧边栏)和 Dialog
(对话框)等展现通用容器(box)的组件中特别容易遇到这种情况。
我们建议这些组件使用一个特殊的 children
prop 来将他们的子组件传递到渲染结果中:
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>
);
}
<FancyBorder>
JSX 标签中的所有内容都会作为一个 children
prop 传递给 FancyBorder
组件。因为 FancyBorder
将 {props.children}
渲染在一个 <div>
中,被传递的这些子组件最终都会出现在输出结果中。
少数情况下,你可能需要在一个组件中预留出几个“洞”。这种情况下,我们可以不使用 children
,而是自行约定:将所需内容传入 props,并使用相应的 prop。
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 />
} />
);
}
<Contacts />
和 <Chat />
之类的 React 元素本质就是对象(object),所以你可以把它们当作 props,像其他数据一样传递。这种方法可能使你想起别的库中“槽”(slot)的概念,但在 React 中没有“槽”这一概念的限制,你可以将任何东西作为 props 进行传递。
特例关系
有些时候,我们会把一些组件看作是其他组件的特殊实例,比如 WelcomeDialog
可以说是 Dialog
的特殊实例。
在 React 中,我们也可以通过组合来实现这一点。“特殊”组件可以通过 props 定制并渲染“一般”组件:
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!" />
);
}
组合也同样适用于以 class 形式定义的组件。 例子见 https://react.docschina.org/docs/composition-vs-inheritance.html
如果想要在组件间复用非 UI 的功能,建议将其提取为一个单独的 JavaScript 模块,如函数、对象或者类。组件可以直接引入(import)而无需通过 extend ***继承***它们。
React 哲学
多图,仅摘小标题,原文见: https://react.docschina.org/docs/thinking-in-react.html
第一步:将设计好的 UI 划分为组件层级
第二步:用 React 创建一个静态版本
第三步:确定 UI state 的最小(且完整)表示
第四步:确定 state 放置的位置
ame=“SplitPane-left”>
{props.left}
{props.right}
);
}
function App() {
return (
<SplitPane
left={
}
right={
} />
);
}
` <Contacts /> ` 和 ` <Chat /> ` 之类的 React 元素本质就是对象(object),所以你可以把它们当作 props,像其他数据一样传递。这种方法可能使你想起别的库中“槽”(slot)的概念,但在 React 中没有“槽”这一概念的限制,你可以将任何东西作为 props 进行传递。
### 特例关系
有些时候,我们会把一些组件看作是其他组件的特殊实例,比如 `WelcomeDialog` 可以说是 `Dialog` 的特殊实例。
在 React 中,我们也可以通过组合来实现这一点。“特殊”组件可以通过 props 定制并渲染“一般”组件:
function Dialog(props) {
return (
{props.title}
{props.message}
);
}
function WelcomeDialog() {
return (
);
}
组合也同样适用于以 class 形式定义的组件。 例子见 https://react.docschina.org/docs/composition-vs-inheritance.html
如果想要在组件间复用非 UI 的功能,建议将其提取为一个单独的 JavaScript 模块,如函数、对象或者类。组件可以直接引入(import)而无需通过 extend ***继承***它们。
## React 哲学
多图,仅摘小标题,原文见: https://react.docschina.org/docs/thinking-in-react.html
### 第一步:将设计好的 UI 划分为组件层级
### 第二步:用 React 创建一个静态版本
### 第三步:确定 UI state 的最小(且完整)表示
### 第四步:确定 state 放置的位置
### 第五步:添加反向数据流