最近看了一本关于学习方法论的书,强调了记笔记和坚持的重要性。这几天也刚好在学习React,所以我打算每天坚持一篇React笔记。
第一节:安装
尝试React
可以在CodePen中尝试React,但是既然选择了React,那就直接下载一个脚手架吧。
创建单页面应用
使用官方脚手架可以快速的创建React单页面应用。它提供了开发环境,可以使用最新的js特性,提供了很好的开发方法以及能够给发布版本做优化。
npm install -g create-react-app
create-react-app hello-world
cd hello-world
npm start
需要注意的是,这个脚手架只是一个前端的脚手架,你可以使用任何的后台。该脚手架使用了webpack,Babel和ESLint,但是你可以自行配置他们。
将React添加到现有应用中
这种情况下,可以在原有应用中,分离出部分单独的界面,用React来尝试。我们也推荐用构建工具来开发,现在的构建工具都包含一下几个工具:
- 包管理工具:Yarn或者npm,它们可以让你尽情的安装、升级三方库
- 打包工具:webpack或者Browserify,它们可以让你组件化开发、将资源打包、优化加载时间
- 编译器:Babel,它将最新的js语法编译为浏览器兼容的版本。
安装React
npm init
npm install --save react react-dom
//todo 了解yarn和npm的区别。
我只知道npm的使用方法,所以我这里我先选择npm。Yarn和Npm都是使用npm的仓库。
使用ES6和JSX
安装Babel就可以了。Babel安装指南说了如何在不同的环境中配置Babel。确认你安装了babel-preset-react和babel-preset-es2015以及在.babelrc
中正确配置好了。
ES6和JSX的HELLO WORLD
强烈建议用webpack和Browserify,这样你可以模块开发。
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
这里将新元素渲染到了root元素中,所以html文件中必须包含这么一个元素。同理,你也可以将它渲染到通过其他Javascript UI库生成的节点中。
开发版本和发布版本
开发版本,react提供了很多有用的警告信息。但是发布的时候,需要用到发布版本。
原文中罗列了Brunch,Browserify,Create React App,Rollup和webpack的优化方法。这里只记录Create React App和Webpack。
Create React App
如果使用了Create React App脚手架,使用npm run build
命令,会在build目录中生成优化版本。
Webpack
根据这篇配置指导来配置,要配置DefinePlugin
和UgligyJsPlugin
。
使用CDN加速
npm包里的dist目录里包含了编译好的库。可以直接拿来用。CDN加速如下:
<script src="https://unpkg.com/react@15/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@15/dist/react-dom.js"></script>
这两个文件只适用于开发,没有优化处理。
适合发布版的优化版本如下所示:
<script src="https://unpkg.com/react@15/dist/react.min.js"></script>
<script src="https://unpkg.com/react-dom@15/dist/react-dom.min.js"></script>
如果想要使用特定版本的react,直接替换里边的版本号15
就可以了
第二节 Hello World
最小的React例子如下:
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
在root节点渲染h1标签
关于js
react是js库,所以要掌握好js。这里推荐js知识给大家。ES6的语法也是可以使用的,但是要谨慎些。推荐学习下箭头函数、模版字符串、let和const。
第三节 JSX介绍
show U the code:
const element = <h1>Hello, world!</h1>;
这个不是html也不是字符串的家伙就是JSX了,它是js的扩展。JSX可能会让你想到模版语言,但是它是不折不扣的js。JSX为React提供渲染所需的“元素”(element)。
在JSX中插入表达式
在JSX中插入js表达式需要用花括号{}括起来:
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中制定属性
const element = <div tabIndex="0"></div>;
const element = <img src={user.avatarUrl}></img>;
以上两种都是可以的:提供一个字符串或者一个表达式。需要注意的是,不要将表达式既用花括号包裹,又用引号包裹,那就出错了。鱼({})和熊掌(‘’)兼得,可能啥也没有哈。(或许有个大bug)
JSX中指定子元素
const element = <img src={user.avatarUrl} />;
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
如果没有子元素,标签可以马上关闭。如果有呢,就像html一样用就好了。
注意:虽然JSX和html很像,但是React DOM 使用驼峰标记法转化html的属性。
例如:class
-->className
,tabindex
-->tabIndex
JSX 防止注入攻击
const title = response.potentiallyMaliciousInput;
// This is safe:
const element = <h1>{title}</h1>;
将用户输入嵌入到JSX中是安全的。默认情况下,在渲染之前,React DOM会转义所有嵌入的值。这保证了只能插入你程序中写好的东西。还有所有的嵌入的都会转换为字符串,有效的防止了XXS(cross-site-scripting)攻击。
JSX 代表对象
babel会把JSX转换为React.createElement()
,所以下面的代码块是一致的
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
React.createElement()
还会帮你检查代码,最终会转换成如下对象:
// Note: this structure is simplified
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world'
}
};
这些对象,就叫做React元素了。用来渲染视图以及更新视图。
提示:强烈建议你搜索下编辑器的“babel”语法方案(syntax scheme),它会让你的JSX和ES6代码高亮显示。
第四节 渲染元素
笔记原文
React元素是构建React应用的最小代码块了。元素描述的就是要渲染的界面了。
const element = <h1>Hello, world</h1>;
React元素和html的dom是不一样的,相比之下,Rect元素更加轻量级。
注意:元素和组件是不同的,组件是由元素构成的。
将元素渲染到DOM中
<div id="root"></div>
你有一个id为root的div,通常React应用都会渲染到这个根结点里。如果你是将React结合到现成的应用里,你也可以有多个节点,用来渲染React元素。
调用ReactDOM.render()
将React元素渲染进DOM节点中。
const element = <h1>Hello, world</h1>;
ReactDOM.render(
element,
document.getElementById('root')
);
更新已经渲染的元素
React元素是不可变的。一旦创建了就不能再更改了。就像是电影里的一帧图像,代表一个时间点固定的图像。
以目前我们所学的,如果要更新界面,就要调用函数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()
。通常我们用state来显示动态UI。
React 只更新需要更新的
更新界面的时候RactDOM会和之前的元素进行比较,并且只会更新改变了的部分。所以上面的代码只会更新时间,其他节点都不会更新。写代码的时候就只要考虑如何显示,不用考虑怎么改变了。
第五节 组件以及属性(props)
date:20170329
笔记原文
组件将这个界面分割成几个独立的,可以重用的部分。开发的时候,可以单独的对一个模块进行思考。从概念上来说,组件很像js函数。它们接收参数(props)并且返回需要渲染的React元素。
函数组件和类组件
我们可以像定义js函数一样定义组件:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
我们也可以用ES6类的语法来定义组件。
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
这两者实现是都是一样的,但是类组件更加强大些,而函数组件会简介些。
渲染组件
之前我们一直渲染的DOM元素,如div。其实React也是可以呈现用户定义的组件:
const element = <Welcome name="Sara" />;
当React遇到用户定义的标记时,会将属性通过对象的方式传递给组件,这个对象就是props。
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Sara" />;
ReactDOM.render(
element,
document.getElementById('root')
);
以上代码,一目了然,直接将Welcome的name属性,封装到props对象里,并且传递给组件,组件在内部,就可以通过props对象,获取到外边传递过来的值。
警告:组件名称首字母一定要大写。小写的是DOM原生标记,大写的代表组件,并且需要将组件引入到作用域中。
组合组件
组件可以引用组件。
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结合到已存在的项目中,最好就是自底向上的方式开发。
警告:组件必须返回一个元素,所以上边的例子需要用div包裹三个Welcome组件。
提取组件
不要担心把组件分割为更小的组件。原文列举了如何把评论界面提取出头像组件,用户信息组件。
细分组件保证了代码重用和降低代码复杂度。
属性只读
Props对象的值都是不能变化的。一种“纯”的函数是不会改变输入的函数,而改变输入参数的函数都是不纯的函数。React是很灵活的框架,但是有一个很苛刻的条件:所有的react组件都必须像纯函数一样,不改变输入的props。
界面是时常要变的,所以就有了State概念。state可以在用户交互的时候,网络响应或者其他情况下,作出界面改变。
State和生命周期
date:20170330
笔记原文
这一节将用state实现时钟组件。最终要求要实现的效果如下:
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
原来的代码如下:
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);
state和props很相似,但是state是私有的,并且是控件自己维护的。之前我们说过的,类组件有一些另外的功能。state是只支持类组件的一种特性。
将函数组件转化为类组件
通过以下5步可以将函数组件转化为类组件。
- 创建一个名字相同的ES6类,该类继承自
React.Component
。 - 添加一个名为
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>
);
}
}
在类中添加State
- 将
render()
中的this.props.date
替换为this.state.date
: - 在类的构造函数中初始化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
- 将原来代码里将Clock的date属性删除,最后的代码如下:
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')
);
在代码里添加生命周期方法
本例中,需要在组件加载(mounting)好之后添加一个计时器,然后在卸载(unmounting)的时候将计时器停止。
componentDidMount()
和componentWillUnmount()
这两个就是生命周期里的回调方法。
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
这里,我们将timerID保存到this中。this.props
是React自身维护的,this.state
具有特殊的含义,一般都用在需要更新界面的地方。我们可以在类里边随意添加不需要在界面上显示的数据。也就是说,不是在render()
中显示的都不需要用state。
实现tick()
,代码如下:
tick() {
this.setState({
date: new Date()
});
}
正确的使用State
这里强调3点关于setState()
的内容
不要直接修改State
如果直接通过this.state.XXX=XXX
来修改内容,界面是不会刷新的。用setState
方法。
State更新是异步的
如下的代码是有问题。
this.setState({
counter: this.state.counter + this.props.increment,
});
this.state
和this.props
可能异步的更新的,所以不应该依赖它们来计算新的state。
这里要修改的话,就把setState的参数,从对象改为函数,代码如下所示:
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
函数第一个参数是之前的state,第二个参数是更新之后的props。上面的函数是箭头函数,也可以普通函数。
State的更新是合并的
这个可以理解,不然,难道要每次更新就把所有的state都列出来吗。
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
以上的代码,都是各自跟新自己的属性,其他的属性都会毫发无伤的维持原样。这就是合并的含义。
向下的数据流
父控件和子控件都不知道一个控件是包含状态的还是不包含状态的,也不关心是函数组件还是类组件。所以state只属于自身控件的,其他控件都访问不了。
一个组件可以通过props
和state
将数据传递到子控件中。
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
<FormattedDate date={this.state.date} />
function FormattedDate(props) {
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
FormattedDate并不关心props中的数据是来自哪里的,可能是state,props或者手写的。
这里,数据流就是自顶向下或者说是单向的。
响应事件
20170331
笔记原文
React响应事件和DOM响应事件是差不多的,但是有些语法差异:
* React的事件命名采用驼峰法,而不是小写
* 在JSX中传递的是一个函数,而不是字符串
<button onclick="activateLasers()">
Activate Lasers
</button>
<button onClick={activateLasers}>
Activate Lasers
</button>
另一个差异就是不能通过返回false来阻止事件,必须手动调用preventDefault
。
例如以下代码可以防止a标签打开新页面:
<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的事件都是通过W3C 定义,所以你也不用担心兼容性。详情见SyntheticEvent.
React中不需要调用addEventListener
,只需要在元素初始化的时候提供一个监听函数。
如果你通过ES6类来定义组件,类中本身有一组模版函数可以使用。例如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')
);
必须要注意的是JSX回调函数中的this
关键字。在js中,类方法并没有默认绑定
到this中。所以如果忘了绑定,那么this.handleClick
的this
就是为定义的。
这个也不是React中的特性,而是js函数运行机理的一部分。
如果引用的时候不用()
,例如onClick={this.handleClick}
,那么你就应该要绑定下。
两种方法可以来绑定this
。第一种是采用试验性功能属性初始化语法(propery initializer syntax)
。
class LoggingButton extends React.Component {
// This syntax ensures `this` is bound within handleClick.
// Warning: this is *experimental* syntax.
handleClick = () => {
console.log('this is:', this);
}
render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
);
}
}
这个语法在官方脚手架Create-Reac-App中默认支持。
第二种方法是使用箭头函数。
class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
// This syntax ensures `this` is bound within handleClick
return (
<button onClick={(e) => this.handleClick(e)}>
Click me
</button>
);
}
}
这个方法的弊端是每次渲染LoggingButton
的时候都会生成一个回调函数。多数情况下是不影响的。但是如果要通过props将回调函数传递给子控件的时候,就会造成多次渲染。为了避免这一类的性能问题,建议用第一种方法或者在构造函数中初始化。
条件渲染
20170331
笔记原文
React中的条件渲染和JS中的条件渲染是一样的。使用[if][js-if]或者条件操作符根据条件来呈现不同的状态。
function UserGreeting(props) {
return <h1>Welcome back!</h1>;
}
function GuestGreeting(props) {
return <h1>Please sign up.</h1>;
}
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')
);
以上的代码就是依靠isLoggedIn
属性来判断渲染不同的界面。
元素变量
可以通过变量来存储元素,以达到部分界面更新,而其他界面不变的目的。
function LoginButton(props) {
return (
<button onClick={props.onClick}>
Login
</button>
);
}
function LogoutButton(props) {
return (
<button onClick={props.onClick}>
Logout
</button>
);
}
下面的代码,我们生成了一个包含状态的组件,命名为LoginControl
。
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')
);
行内if操作符结合&&操作符
你可以在JSX中结合任何的表达式,表达式需要用花括号包裹起来。这是语法。结合&&
可以实现条件渲染。
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')
);
这个原因很简单哈。短路与就是这么神奇的。true && expression
返回true
,false && expression
返回false
.因此,条件满足了&&
后面的内容就会渲染出来,否则就直接忽略了。
行内If-Else条件操作符
另一个条件渲染的方法是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>
);
}
如果条件比较复杂的时候,记得把组件抽取出子组件。
隐藏组件
通过返回null
的方式,来阻止渲染。
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')
);
render
函数返回null
的时候并不会触发组件的生命周期方法。但是componentWillUpdate
和componentDidUpdate
还是会被调用到。
列表与键(key)
20170401
[笔记原文][]
我们先看看js如何改变一个数组,直接上代码:
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((number) => number * 2);
console.log(doubled);
在控制台输出[2,4,6,8,10]
,React中对数组中的[元素(element)][react-element]的处理也是如此。
渲染多个组件
还是使用map
函数,遍历numbers
数组,每次返回一个li
元素。最后我们把数组存储在listItem
中。
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li>{number}</li>
);
然后我们将listItems
用<ul>
元素包裹起来,最后[渲染到DOM][react-render]中。
ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
);
基本的列表组件
通常我们需要把在[组件][react-component]中渲染一个列表。
重构以上的代码:
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')
);
在跑这段代码的时候,它会警告你需要给列表项提供一个键(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用来判断列表项是否有改变,添加以及删除。所以我们在列表数据中,需要添加一个固定的项:
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
在一个列表中最好指定一个唯一确定的数值。所以通常使用数据的ID就可以了。
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);
如果没有id,那也可以使用索引。
const todoItems = todos.map((todo, index) =>
// Only do this if items have no stable IDs
<li key={index}>
{todo.text}
</li>
);
如果列表的数据顺序会变的情况下,我们是不推荐使用索引的,因为速度慢。参看[关于键的高级进阶][react-key-explanation]。
与键一起抽取组件
键只有在列表的上下文中才有意义。
例如,我们抽取了ListItem
组件,我们需要将键同时抽出,放置在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')
);
总之,map()
里的元素需要键。
在一个列表中键必须唯一
同一个列表的键必须唯一,但是在全局上,不同的列表就没有限制了。
由于key是react的机制,key的值并不会传递到组件props里,所以如果需要使用数据的时候,要再传递一次。
const content = posts.map((post) =>
<Post
key={post.id}
id={post.id}
title={post.title} />
);
Post
组件可以获取到props.id
,但是获取不到props.key
。
在JSX中嵌入map()
之前的例子我们用一个变量来存储列表元素。JSX是可以[嵌入任何表达式][react-embed-expressions]的,所以我们将map()
函数直接嵌入到JSX中。
function NumberList(props) {
const numbers = props.numbers;
return (
<ul>
{numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
)}
</ul>
);
}
这种方法很清晰,但是不能滥用。如果map()
中嵌入太多,就需要[抽取组件][react-extract-component]了
表单
date:20170402
笔记原文
表单和其他元素有些不同,因为表单包含了一些交互。通常我们会用js来控制提交信息。所以React提供了控制组件
控制组件
html中的表单组件通常都是自己维护用户输入,但是在React中,只能用setState()
。
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>
);
}
}
每当表单控件变化的时候,就会触发onChange事件,从而调用监听方法。在监听方法中,改变state中的值。
textarea标签
React中,<textarea>
标签也会有value
属性,这样使用起来会比较方便。
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>
);
}
}
select 标签
在html中,select能够生成一个下菜单:
<select>
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option selected value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
注意,Coconut
这一项中,具有select
属性来指定默认选择。在React中,是value
属性。所以我们只要更新state就可以了。
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>
);
}
}
总之,<input type="text">
,<textarea>
和<select>
都是类似的机制,
监听函数处理多个表单元素输入
如果要实现这样的功能,可以在表单元素里添加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>
);
}
}
这里要留意下我们使用了ES6的计算属性名称(computed property name)。下面两段代码都是一样的效果。
this.setState({
[name]: value
});
var partialState = {};
partialState[name] = value;
this.setState(partialState);
同时,由于setState()
自动的混合部分属性,所以也只是更新变化的部分。
替换控制组件
如果有时候写控制组件太麻烦了,或者说要从非React代码变更为React的时候,你也可以考虑下非控制组件,这是控制组件的替代技术。
玩转State
date:20170404
笔记原文
通常很多组件都要使用同一条数据,这时候,就要把数据提出来,放在这些控件最近的父组件中。
举个温度计例子:BoilingVerdict
组件接收一个温度参数,返回水是否会沸腾。
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>
);
}
}
添加第二个输入框
新需求是添加一个华氏度的输入框,要求华氏温度和摄氏温度同时变化。刚开始,我们从Calculator
中提取TemperatureInput
组件,添加scale
prop属性,来指示温度的单位。
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>
);
}
}
现在虽然有了两个输入框,但是数据还是不会更新,同时BoilingVerdict
也没有显示出来,因为输入的温度,都在TemperatureInput
里,
添加转换功能
首先,写几个功能函数,实现温度互相转换:
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
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();
}
提出State
目前,两个TemperatureInput
控件里的数据都是在它们自己的控件中。这是满足不了需求的。在React中,State共享是将数据提出到最近的父容器中。我们称之为提出State
。我们需要将TemperatureInput
中的state提出到Calculator
。这样,两个输入控件具有相同的数据源,能够实现同步变化的需求。
一步一步来解析如何实现这个功能。
第一步,将state
替换为prop
,从Calculator
传递数据到输入控件中。
render() {
// Before: const temperature = this.state.temperature;
const temperature = this.props.temperature;
我们知道,props是只读的。如果是state的话,直接调用this.setState()
就好了。但是prop的话就没有办法控制了。
在React中,类比之前的控制组件
,就像<input>
标签有value
和onChange
属性,所以TemperatureInput
可以从父控件Caculator
里传入temperature
和onTemperatureChange
属性,这样,TemperatureInput
想要跟新温度的时候,就可以通过调用this.props.onTemperatureChange
:
handleChange(e) {
// Before: this.setState({temperature: e.target.value});
this.props.onTemperatureChange(e.target.value);
这里函数名称是可以自己随便定义的,而不是定死的。onTemperatureChange
作为回调,是从父控件与温度数据一同传进来的。调用的时候,就会相应的改变父容器的state,然后更新视图。
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
组件中,将温度和单位存储在state中。这些state是从input
组件里提取出来的。这是满足需求的最少数据集。例如,当我们在摄氏度一栏输入37,数据如下:
{
temperature: '37',
scale: 'c'
}
当我们在华氏度里输入212,数据如下:
{
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>
);
}
}
好了,功能实现了。我们总结下,我们具体做了哪些工作:
- 在
TemperatureInput
组件中,<input>
中指定onChange
的回调函数 - 在上述
onChange
回调中,执行父容器提供的回调this.props.onTemperatureChange()
,将变化值传递给父容器。 - 两个输入控件都有自己的回调函数
- 输入控件的回调函数中,将子控件传递过来的新的数据,通过
setState
来重新设置, - 根据当前的温度和单位,渲染界面
他山之石
React奉行的是单一数据源原则。如果数据可能要有变化,那就使用State。如果这个数据其他控件需要用到,那么就提出State。如果要在不同的控件中同步数据,那么就要遵循单向数据流。
从数据上来说,如果一个数据可以从其他数据中计算得到,那么就不用保存在state中。这个例子中,我们只是保存了一个输入的数据。
React提供了一套React调试开发工具来查看属性和状态更新,有利于调试代码。
组合VS继承
date:20170405
笔记原文
建议不用继承来复用代码,而是用组合模式。
控制
有些组件并不知道他们要包含的子组件。例如幻灯片和对话框就是这样。
我们建议这类组件为这类组件创建特殊的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标签都会通过{props.children}
属性传递到FancyBorder
中。由于在FancyBorder
里的<div>
中渲染了{props.children}
,最后这些子标签就出现在了页面中。
有时候,我们需要在组件里定义插槽,通过插入不同的组件,就可以实现不同的效果。这样就不用使用props.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 />
} />
);
}
这里我们像传递数据一样,传递控件。
特殊化
有时候我们需要将一些组件特殊化。例如欢迎对话框
是特殊化的对话框
了。在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!" />
);
}
组合也同样适用于类组件:
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}!`);
}
}
那么继承呢?
facebook通过成百上千个组件构成,但是还没有用到需要用到继承的情况。Props和组合已经具有足够的灵活性和安全性来自定义控件。记住,porps可以传递原型数据,React元素和函数。
如果想要服用功能函数,建议用单独的js模块来分离代码,实现复用。
Think in React笔记
date:20170405
笔记原文
得益于react的模块化,我们可以很方便的开发大型web应用。
从设计稿开始
- 将设计稿根据功能分块处理。根据单一性原则,分为不能再精简的单一模块。
- 用固定的数据生成静态页面,因为我们不需要思考交互。
- 简单的页面可以通过自顶向下的方式开发,复杂的页面可以通过自底向上的方式开发
- 这里不需要用到state,通过props将数据传递到模块中。
- 确认数量最少但是能满足条件的state
- DRY原则:能够从元数据获取到的信息,就不要提炼出state。如TodoList的长度,直接使用数组的长度,而不是提炼出新的state。
- 如何分辨需要state还是props:是否是父控件通过props传递进来的;是否是一成不变的信息;是否可以通过其他信息得到。
- 确认state应该在哪里,react的数据是单向的,新手可以通过以下步骤去掌握它。
- 确认每个组件依赖的state
- 找到一个共同的父控件
- 共同父控件或者该父控件的父控件拥有这个state
- 如果你不知道将这个state放在哪里的时候,就新建一个控件,专门来维护这个state,然后将这个控件放置在共同父控件的上层
- 添加反向数据流,通过底层控件改变上层控件的状态
这就是React
代码写的比平常多了,但是在构建大项目的时候,你就会发现好处了