React Quick Start笔记

最近看了一本关于学习方法论的书,强调了记笔记和坚持的重要性。这几天也刚好在学习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,BabelESLint,但是你可以自行配置他们。

将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-reactbabel-preset-es2015以及在.babelrc中正确配置好了。

ES6和JSX的HELLO WORLD

强烈建议用webpackBrowserify,这样你可以模块开发。

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

根据这篇配置指导来配置,要配置DefinePluginUgligyJsPlugin

使用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的语法也是可以使用的,但是要谨慎些。推荐学习下箭头函数模版字符串letconst


第三节 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步可以将函数组件转化为类组件。

  1. 创建一个名字相同的ES6类,该类继承自React.Component
  2. 添加一个名为render()的函数。
  3. 将函数体内的代码添加到render()函数里。
  4. 将代码里的props替换为this.props
  5. 将剩下的函数体删除。
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

在类中添加State

  1. render()中的this.props.date替换为this.state.date
  2. 类的构造函数中初始化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

  1. 将原来代码里将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.statethis.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只属于自身控件的,其他控件都访问不了。
一个组件可以通过propsstate将数据传递到子控件中。

<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.handleClickthis就是为定义的。
这个也不是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返回truefalse && 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的时候并不会触发组件的生命周期方法。但是componentWillUpdatecomponentDidUpdate还是会被调用到。


列表与键(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组件,添加scaleprop属性,来指示温度的单位。

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>标签有valueonChange属性,所以TemperatureInput可以从父控件Caculator里传入temperatureonTemperatureChange属性,这样,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应用。

从设计稿开始

  1. 将设计稿根据功能分块处理。根据单一性原则,分为不能再精简的单一模块。
  2. 用固定的数据生成静态页面,因为我们不需要思考交互。
    • 简单的页面可以通过自顶向下的方式开发,复杂的页面可以通过自底向上的方式开发
    • 这里不需要用到state,通过props将数据传递到模块中。
  3. 确认数量最少但是能满足条件的state
    • DRY原则:能够从元数据获取到的信息,就不要提炼出state。如TodoList的长度,直接使用数组的长度,而不是提炼出新的state。
    • 如何分辨需要state还是props:是否是父控件通过props传递进来的;是否是一成不变的信息;是否可以通过其他信息得到。
  4. 确认state应该在哪里,react的数据是单向的,新手可以通过以下步骤去掌握它。
    • 确认每个组件依赖的state
    • 找到一个共同的父控件
    • 共同父控件或者该父控件的父控件拥有这个state
    • 如果你不知道将这个state放在哪里的时候,就新建一个控件,专门来维护这个state,然后将这个控件放置在共同父控件的上层
  5. 添加反向数据流,通过底层控件改变上层控件的状态

这就是React

代码写的比平常多了,但是在构建大项目的时候,你就会发现好处了


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

推荐阅读更多精彩内容