React学习

React学习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}

export default App;

这个是 React 初始化后的模板。

npm start 后是一个 React Logo 的动画页面。

首先,这个页面没有路由,只是一个简单页面而已,不管是什么 url 都会指向这个默认页面。

整个项目的入口是 index.jsApp.js 只是一个组件而已,通过 import App from './App'; 引入,这里的 .js 后缀是省略掉的。

从入口文件来看,入口函数是 ReactDOM.render() ,函数的作用就是将函数内的代码渲染页面,<React.StrictMode> 标签表示这里面是用的严格模式,然后是主组件 App 的标签。

ReactDOM.render(template,targetDOM) 方法接收两个参数:

  • 第一个是创建的模板,多个 dom 元素外层需使用一个标签进行包裹,如 <div>;
  • 第二个参数是插入该模板的目标位置。

也就是说,这个函数其实并不是只能运行一次,而是可以运行多次,也就可以通过这种形式插入多个组件。

但是正常来说,更希望能够使用组件的方式进行调用及渲染。单独渲染一个组件可能会带来多余的消耗。

然后看 App.js ,首先它使用了一个函数式的编程,将页面代码写在 JavaScript 函数里面,返回这个页面对象,再通过 export default 将这个函数导出,这样一个单文件组件就写好了。这样我们可以使用一些子组件在这个组件里面,但是并不导出,作为一个私有方法。

App.js 里面的 CSS 就不说了,就是简单的动画而已,才了解到的就只有 prefers-reduced-motion 了,这个是用来判断用户是否开启了动画减弱功能。

JSX

在上面的 App.js 中,组件的代码是通过函数返回对象的方式使用的。

1
const element = <h1>Hello, {name}</h1>;

这种形式,就是 JSX。

通过这种方法可以将一些复用性高的 HTML 代码变成 JavaScript 对象。在使用中调用对象即可。

1
const element = <img src={user.avatarUrl}></img>;

在 JSX 中,可以通过 {} 插入 JavaScript 表达式。

你可以安全地在 JSX 当中插入用户输入内容:

1
2
3
const title = response.potentiallyMaliciousInput;// 这个变量有点离谱,实际上就是说是恶意代码,并不是说是一个从 response 对象上获取的属性,意思就是用户上传后获取到的用户输入的恶意代码。
// 直接使用是安全的:
const element = <h1>{title}</h1>;

React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止 XSS(cross-site-scripting, 跨站脚本)攻击。

Babel 会把 JSX 转译成一个名为 React.createElement() 函数调用。

以下两种示例代码完全等效:

1
2
3
4
5
6
7
8
9
10
const element = (
<h1 className="greeting"> {/*是用的 className 注意驼峰写法。不是 class。*/}
Hello, world!
</h1>
);
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);

React.createElement() 会预先执行一些检查,以帮助你编写无错代码,但实际上它创建了一个这样的对象:

1
2
3
4
5
6
7
8
// 注意:这是简化过的结构
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
};

组件

定义组件最简单的方式就是编写 JavaScript 函数:

1
2
3
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}

该函数是一个有效的 React 组件,因为它接收唯一带有数据的 “props”(代表属性)对象与并返回一个 React 元素。这类组件被称为“函数组件”,因为它本质上就是 JavaScript 函数。

自定义组件

1
const element = <Welcome name="Sara" />;

当 React 元素为用户自定义组件时,它会将 JSX 所接收的属性(attributes)以及子组件(children)转换为单个对象传递给组件,这个对象被称之为 “props”。

例如,这段代码会在页面上渲染 “Hello, Sara”:

1
2
3
4
5
6
7
8
9
function Welcome(props) {  
return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Sara" />;
ReactDOM.render(
element,
document.getElementById('root')
);

组件应该尽可能的小或者只包含一个元素。

Props 只读

组件无论是使用函数声明还是通过 class 声明,都决不能修改自身的 props。来看下这个 sum 函数:

1
2
3
function sum(a, b) {
return a + b;
}

这样的函数被称为“纯函数”,因为该函数不会尝试更改入参,且多次调用下相同的入参始终返回相同的结果。

相反,下面这个函数则不是纯函数,因为它更改了自己的入参:

1
2
3
function withdraw(account, amount) {
account.total -= amount;
}

React 非常灵活,但它也有一个严格的规则:

所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。

函数组件和类组件

看了不少实例代码,感觉函数组件通常用来返回一些基本 JSX 元素,通过类组件去进行调用和组合。

State

State 与 props 类似,但是 state 是私有的,并且完全受控于当前组件。

上面说过,组件是一个 JavaScript 函数,当然 Class 类也是可以的,但是需要继承 React.Component 。参考 https://react.docschina.org/docs/react-component.html

1
2
3
4
5
6
7
8
9
10
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

在 Class 类中使用 state 就需要通过 Class 的 Constructor 构造器来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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>
);
}
}

这个 state 不能被直接修改,直接修改并不会重新渲染元素。

1
2
// Wrong
this.state.comment = 'Hello';

而是应该使用 setState():

1
2
// Correct
this.setState({comment: 'Hello'});

只有 Constructor 里面能赋值。

如果 setState 需要参与计算,需要使用函数。

1
2
3
4
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));

关于 state 可以参考一下 https://www.jianshu.com/p/a883552c67de 了解更多细节。

数据自顶向下

父组件或子组件都不能知道某个组件是有状态还是无状态,并且它们不应该关心某组件是被定义为一个函数还是一个类。

这就是为什么状态通常被称为局部或封装。 除了拥有并设置它的组件外,其它组件不可访问。

也就是说数据在改变之后只会影响或更新当前组件和他的子组件。和 Vue 的组件更新顺序应该是一样的。

渲染过程:
父组件挂载完成一定是等子组件都挂载完成后,才算是父组件挂载完,所以父组件的mounted在子组件mouted之后
父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted

子组件更新过程:

1. 影响到父组件: 父beforeUpdate -> 子beforeUpdate->子updated -> 父updted
2. 不影响父组件: 子beforeUpdate -> 子updated

父组件更新过程:

1. 影响到子组件: 父beforeUpdate -> 子beforeUpdate->子updated -> 父updted
2. 不影响子组件: 父beforeUpdate -> 父updated

销毁过程:
父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed

生命周期

参考 https://www.runoob.com/react/react-component-life-cycle.html

组件的生命周期可分成三个状态:

  • Mounting:已插入真实 DOM
  • Updating:正在被重新渲染
  • Unmounting:已移出真实 DOM

生命周期的方法有:

  • componentWillMount 在渲染前调用,在客户端也在服务端。
  • componentDidMount : 在第一次渲染后调用,只在客户端。之后组件已经生成了对应的DOM结构,可以通过this.getDOMNode()来进行访问。 如果你想和其他JavaScript框架一起使用,可以在这个方法中调用setTimeout, setInterval或者发送AJAX请求等操作(防止异步操作阻塞UI)。
  • componentWillReceiveProps 在组件接收到一个新的 prop (更新后)时被调用。这个方法在初始化render时不会被调用。
  • shouldComponentUpdate 返回一个布尔值。在组件接收到新的props或者state时被调用。在初始化时或者使用forceUpdate时不被调用。
    可以在你确认不需要更新组件时使用。
  • componentWillUpdate在组件接收到新的props或者state但还没有render时被调用。在初始化时不会被调用。
  • componentDidUpdate 在组件完成更新后立即调用。在初始化时不会被调用。
  • componentWillUnmount在组件从 DOM 中移除之前立刻被调用。

事件处理

  • React 事件的命名采用小驼峰式(camelCase),而不是纯小写。
  • 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。

例如,传统的 HTML:

1
2
3
<button onclick="activateLasers()">
Activate Lasers
</button>

在 React 中略微不同:

1
<button onClick={activateLasers}> Activate Lasers </button>

当你使用 ES6 class 语法定义一个组件的时候,通常的做法是将事件处理函数声明为 class 中的方法。例如,下面的 Toggle 组件会渲染一个让用户切换开关状态的按钮:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};

// 为了在回调中使用 `this`,这个绑定是必不可少的。绑定方法到 Toggle 类的 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')
);

官方文档中还介绍了其他两种实验性语法,不过还是通过绑定到类上比较好一些。

传参

在循环中,通常我们会为事件处理函数传递额外的参数。例如,若 id 是你要删除那一行的 ID,以下两种方式都可以向事件处理函数传递参数:

1
2
<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 的方式,事件对象以及更多的参数将会被隐式的进行传递。

条件渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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')
);

这里通过参数传递到 Greeting 组件,通过 if 语句返回不同的子组件。通过不同的子组件去实现条件渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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};
}
// 提供方法修改 state。
handleLoginClick() {
this.setState({isLoggedIn: true});
}

handleLogoutClick() {
this.setState({isLoggedIn: false});
}

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')
);

如果是有状态组件的话通过状态也是可以实现。通过将元素保存为变量去使用,减少代码的繁琐。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 表达式,可以使用 && || 三目运算符等运算符,结合 JSX 完成条件渲染。

1
2
3
4
5
6
7
8
9
10
11
render() {
const isLoggedIn = this.state.isLoggedIn;
return (
<div>
{isLoggedIn
? <LogoutButton onClick={this.handleLogoutClick} />
: <LoginButton onClick={this.handleLoginClick} />
}
</div>
);
}

这样写虽然也挺好的,但是可读性不太行,但是更加的简洁。

阻止组件渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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')
);

在这个实例中,通过对参数进行判断,返回 null,即可阻止组件渲染,

在组件的 render 方法中返回 null 并不会影响组件的生命周期。例如,上面这个示例中,componentDidUpdate 依然会被调用。

意思应该是返回 null 只是会不渲染这个组件而已,依然会完整走完整个 Mounting、Updating、Unmounting 的生命周期。

列表

1
2
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) => <li>{number}</li>);

我们把整个 listItems 插入到 <ul> 元素中,然后渲染进 DOM

1
2
3
4
ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
);

将这个方法整合进一个组件内,就可以通过这个组件快速生成列表形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 快速定位元素,优化性能。Vue 中的 for 循环也有类似的设计,在大型列表中很有效。

如果列表中不包含唯一标识,可以使用 列表索引 Index 来标识。但是最好不要,所以后端返回数据时附带一个唯一标识是最好的选择。

关于 key 可以参考 https://medium.com/@robinpokorny/index-as-a-key-is-an-anti-pattern-e0349aece318

https://zh-hans.reactjs.org/docs/reconciliation.html#recursing-on-children

元素的 key 只有放在就近的数组上下文中才有意义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 正确的使用 key 的方式
// 通过函数式组件返回最小基本元素的 JSX
function ListItem(props) {
// 正确!这里不需要指定 key:
return <li>{props.value}</li>;
}

function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 正确!key 应该在数组的上下文中被指定
<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 属性。

JSX 允许在大括号中嵌入任何表达式,所以我们可以内联 map() 返回的结果,相比上面的实例可以节省变量的内存空间。

1
2
3
4
5
6
7
8
9
10
11
function NumberList(props) {
const numbers = props.numbers;
return (
<ul>
{numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
)}
</ul>
);
}

表单

受控组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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>
);
}
}

很多东西都可以做成组件。通过以上代码将表单写成具有 state 的组件,绑定到表单的 value 值,将 onChange 事件绑定为组件方法。这里通过绑定 valueonChange 完成了一个双向绑定,和 v-model 相似。

这里使用了 ES6 计算属性名称的语法更新给定输入名称对应的 state 值:

例如:

1
2
this.setState({
[name]: value});

等同 ES5:

1
2
3
var partialState = {};
partialState[name] = value; // 这里并不是数组,而是对象的键名,上面那个也是一样。
this.setState(partialState);

状态提升

通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。

在 React 中,将多个组件中需要共享的 state 向上移动到它们的最近共同父组件中,便可实现共享 state。这就是所谓的“状态提升”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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>
);
}
}

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>
);
}
}

这个状态提升算是一种思想,如果某一组件需要重复多次使用,同时某一属性具有一定的共享性,就可以通过将属性提升到父组件内,将两个子组件的共享内容同步。

在 React 应用中,任何可变数据应当只有一个相对应的唯一“数据源”。通常,state 都是首先添加到需要渲染数据的组件中去。然后,如果其他组件也需要这个 state,那么你可以将它提升至这些组件的最近共同父组件中。你应当依靠自上而下的数据流,而不是尝试在不同组件间同步 state。

虽然提升 state 方式比双向绑定方式需要编写更多的“样板”代码,但带来的好处是,排查和隔离 bug 所需的工作量将会变少。由于“存在”于组件中的任何 state,仅有组件自己能够修改它,因此 bug 的排查范围被大大缩减了。此外,你也可以使用自定义逻辑来拒绝或转换用户的输入。

组合与继承

组合

有些组件无法提前知晓它们子组件的具体内容。在 Sidebar(侧边栏)和 Dialog(对话框)等展现通用容器(box)的组件中特别容易遇到这种情况。

我们建议这些组件使用一个特殊的 children prop 来将他们的子组件传递到渲染结果中:

1
2
3
4
5
6
7
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}

这使得别的组件可以通过 JSX 嵌套,将任意组件作为子组件传递给它们。

少数情况下,你可能需要在一个组件中预留出几个“洞”。这种情况下,我们可以不使用 children,而是自行约定:将所需内容传入 props,并使用相应的 prop。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 />
} />
);
}

说到底还是 JSX 这个格式太灵活了。

这种方法可能使你想起别的库中“槽”(slot)的概念,但在 React 中没有“槽”这一概念的限制,你可以将任何东西作为 props 进行传递。

到处都有 diss 友商

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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}!`);
}
}

class 组件也是可以组合的。上面的 Dialog 是那个组件,并不存在这个标签的。FancyBorder 是最上面的那个组件。

继承

在 Facebook,我们在成百上千个组件中使用 React。我们并没有发现需要使用继承来构建组件层次的情况。

Props 和组合为你提供了清晰而安全地定制组件外观和行为的灵活方式。注意:组件可以接受任意 props,包括基本数据类型,React 元素以及函数。

如果你想要在组件间复用非 UI 的功能,我们建议将其提取为一个单独的 JavaScript 模块,如函数、对象或者类。组件可以直接引入(import)而无需通过 extend 继承它们。

小结

这么牛皮的框架不需要组件继承

高级一点的 React 文档

无障碍

这里的无障碍更多的还是对 Aria 的支持,并在 react 中使用原生的 HTML 的带连字符的命名法。

有时,语义化的 HTML 会被破坏。比如当在 JSX 中使用 <div> 元素来实现 React 代码功能的时候,又或是在使用列表(<ol><ul><dl>)和 HTML <table> 时。 在这种情况下,我们应该使用 React Fragments 来组合各个组件。

举个例子,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React, { Fragment } from 'react';
function ListItem({ item }) {
return (
<Fragment>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</Fragment> );
}

function Glossary(props) {
return (
<dl>
{props.items.map(item => (
<ListItem item={item} key={item.id} />
))}
</dl>
);
}
function ListItem({ item }) {
return (
<>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</>
);
}

这里的 <Fragment> 标签并不是一个实体标签,更像是一个空标签,由于 JSX 需要返回的是一个标签,通常我们使用 <div> 来包裹住自己的组件,但是有的时候需要返回多个同级标签,使用 <div> 会破坏结构,就需要使用 <Fragment> 标签来包裹。简写为 <>

代码分隔

在你的应用中引入代码分割的最佳方式是通过动态 import() 语法。

使用之前:

1
2
3
import { add } from './math';

console.log(add(16, 26));

使用之后:

1
2
3
import("./math").then(math => {
console.log(math.add(16, 26));
});

当 Webpack 解析到该语法时,会自动进行代码分割。如果你使用 Create React App,该功能已开箱即用,你可以立刻使用该特性。Next.js 也已支持该特性而无需进行配置。

React.lazy()

React.lazy 函数能让你像渲染常规组件一样处理动态引入(的组件)。

使用之前:

1
import OtherComponent from './OtherComponent';

使用之后:

1
const OtherComponent = React.lazy(() => import('./OtherComponent'));

此代码将会在组件首次渲染时,自动导入包含 OtherComponent 组件的包。

React.lazy 接受一个函数,这个函数需要动态调用 import()。它必须返回一个 Promise,该 Promise 需要 resolve 一个 defalut export 的 React 组件。

然后应在 Suspense 组件中渲染 lazy 组件,如此使得我们可以使用在等待加载 lazy 组件时做优雅降级(如 loading 指示器等)。

fallback 属性接受任何在组件加载过程中你想展示的 React 元素。你可以将 Suspense 组件置于懒加载组件之上的任何位置。你甚至可以用一个 Suspense 组件包裹多个懒加载组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { Suspense } from 'react'; // 注意 Suspense 是官方的一个组件。提供 fallback 属性。

const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
</div>
);
}

基于路由的代码分割

决定在哪引入代码分割需要一些技巧。你需要确保选择的位置能够均匀地分割代码包而不会影响用户体验。

一个不错的选择是从路由开始。大多数网络用户习惯于页面之间能有个加载切换过程。你也可以选择重新渲染整个页面,这样您的用户就不必在渲染的同时再和页面上的其他元素进行交互。

这里是一个例子,展示如何在你的应用中使用 React.lazyReact Router 这类的第三方库,来配置基于路由的代码分割。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));

const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
</Switch>
</Suspense>
</Router>
);

命名导出(Named Exports)

React.lazy 目前只支持默认导出(default exports)。如果你想被引入的模块使用命名导出(named exports),你可以创建一个中间模块,来重新导出为默认模块。这能保证 tree shaking 不会出错,并且不必引入不需要的组件。

1
2
3
4
5
6
7
8
// ManyComponents.js
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;
// MyComponent.js 这个就是中间模块,由于上面的模块是命名导出,不能用于 lazy() 函数,通过中间模块再导出即可。
export { MyComponent as default } from "./ManyComponents.js";
// MyApp.js
import React, { lazy } from 'react';
const MyComponent = lazy(() => import("./MyComponent.js"));

Context

Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。举个例子,在下面的代码中,我们通过一个 “theme” 属性手动调整一个按钮组件的样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class App extends React.Component {
render() {
return <Toolbar theme="dark" />;
}
}

function Toolbar(props) {
// Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
// 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
// 因为必须将这个值层层传递所有组件。
return (
<div>
<ThemedButton theme={props.theme} />
</div> );
}

class ThemedButton extends React.Component {
render() {
return <Button theme={this.props.theme} />;
}
}

使用 context, 我们可以避免通过中间元素传递 props:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
// 在这个例子中,我们将 “dark” 作为当前的值传递下去。
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}

class ThemedButton extends React.Component {
// 指定 contextType 读取当前的 theme context。
// React 会往上找到最近的 theme Provider,然后使用它的值。
// 在这个例子中,当前的 theme 值为 “dark”。 static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
// 这代码总觉得顺序反了。反过来看就看的好些了,ThemedButton 里面使用 this.context,向上找 Provider,跨过 Toolbar 找到 App 里面,

上面的代码可能不太好理解,关键点在于 React.createContext('light'); 首先,这个 Context 是一个对象,下面的 <ThemeContext.Provider> 标签是一个 ContextProvider 用法。

当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。

只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。这有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。

Context.Provider

1
<MyContext.Provider value={/* 某个值 */}>

每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。

Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。

当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。

通过新旧值检测来确定变化,使用了与 Object.is 相同的算法。

Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。

如果你只是想避免层层传递一些属性,组件组合(component composition)有时候是一个比 context 更好的解决方案。

错误边界

错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。

也就是说错误边界旨在提高整个网页的容错率,即使出现错误也可以采取备用的组件。

注意

错误边界无法捕获以下场景中产生的错误:

  • 事件处理(了解更多
  • 异步代码(例如 setTimeoutrequestAnimationFrame 回调函数)
  • 服务端渲染
  • 它自身抛出来的错误(并非它的子组件)

如果一个 class 组件中定义了 static getDerivedStateFromError()componentDidCatch() 这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你同样可以将错误日志上报给服务器
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}

然后你可以将它作为一个常规组件去使用:

1
2
3
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>

错误边界无法捕获自己产生的错误,所以在常规组件中添加 getDerivedStateFromError()componentDidCatch() 是无效的。常用做法就是像上面一样写一个错误边界组件出来放在整个应用的顶层。

Refs 转发

在 Vue 中,ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例:

1
2
3
4
5
<!-- `vm.$refs.p` will be the DOM node -->
<p ref="p">hello</p>

<!-- `vm.$refs.child` will be the child component instance -->
<child-component ref="child"></child-component>

v-for 用于元素或组件的时候,引用信息将是包含 DOM 节点或组件实例的数组。

关于 ref 注册时间的重要说明:因为 ref 本身是作为渲染结果被创建的,在初始渲染的时候你不能访问它们 - 它们还不存在!$refs 也不是响应式的,因此你不应该试图用它在模板中做数据绑定。

所以一般用法是通过 this.$refs 访问子组件实例和子元素并进行操作。

在 React 中,效果是差不多的。

Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。

在下面的示例中,FancyButton 使用 React.forwardRef 来获取传递给它的 ref,然后转发到它渲染的 DOM button

1
2
3
4
5
6
7
const FancyButton = React.forwardRef((props, ref) => (  <button ref={ref} className="FancyButton">    {props.children}
</button>
));

// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

这样,使用 FancyButton 的组件可以获取底层 DOM 节点 button 的 ref ,并在必要时访问,就像其直接使用 DOM button 一样。

以下是对上述示例发生情况的逐步解释:

  1. 我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量。
  2. 我们通过指定 ref 为 JSX 属性,将其向下传递给 <FancyButton ref={ref}>
  3. React 传递 refforwardRef 内函数 (props, ref) => ...,作为其第二个参数。
  4. 我们向下转发该 ref 参数到 <button ref={ref}>,将其指定为 JSX 属性。
  5. 当 ref 挂载完成,ref.current 将指向 <button> DOM 节点。

注意

第二个参数 ref 只在使用 React.forwardRef 定义组件时存在。常规函数和 class 组件不接收 ref 参数,且 props 中也不存在 ref

Ref 转发不仅限于 DOM 组件,你也可以转发 refs 到 class 组件实例中。

所以 Ref 的实际用法和 Vue 中是一样的,通过这个属性来访问子组件和元素。

Fragments

之前提到过,通过 <React.Fragment> 标签包裹的 jsx,会在返回的时候去掉这个标签,将子列表完整返回。

一种常见模式是组件返回一个子元素列表。以此 React 代码片段为例:

1
2
3
4
5
6
7
8
9
10
11
class Table extends React.Component {
render() {
return (
<table>
<tr>
<Columns />
</tr>
</table>
);
}
}

<Columns /> 需要返回多个 <td> 元素以使渲染的 HTML 有效。如果在 <Columns />render() 中使用了父 div,则生成的 HTML 将无效。

为了方便可以使用 <> 空标签作为短写。

高阶组件(HOC)

高阶组件是参数为组件,返回值为新组件的函数。

在这些”高级指引“中,这些功能或属性都是为了解决一个实际操作中的痛点来设计实现的。比如之前的 FragmentContext

我们之前建议使用 mixins 用于解决横切关注点相关的问题。但我们已经意识到 mixins 会产生更多麻烦。阅读更多 以了解我们为什么要抛弃 mixins 以及如何转换现有组件。

在 react 官方文档中说的是”使用 HOC 解决横切关注点问题“,那么我们首先要知道什么是横切关注点问题

横切关注点指的是一些具有横越多个模块的行为,使用传统的软件开发方法不能够达到有效的模块化的一类特殊关注点。

简单说就是我们希望某一项功能或者属性是可以给多个组件添加的,在不考虑组件间的关系的前提下,传统模式我们就只能一个一个去复制粘贴,这样违背了模块化的要求。

在 Vue 中所采用的一种方法就是 mixins(混入)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义一个混入对象
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}

// 定义一个使用混入对象的组件
var Component = Vue.extend({
mixins: [myMixin]
})

var component = new Component() // => "hello from mixin!"

在 Vue 的 mixins 中,我们能感觉到很强烈的继承的味道。新的组件继承了 myMixin 组件,这里是说的组件而不是 mixins ,因为他确实就是一个普通组件,只是用作 mixins 而已。新的组件继承了生命周期函数和普通方法。

更加详细的参考 https://cn.vuejs.org/v2/guide/mixins.html,Vue2 的文档。

回到 react 中来,以前是也用过 mixins 的,但是考虑到它所带来的问题还是放弃了,改用 HOC(高阶组件)。

请注意,HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用。

正如文档所说的,HOC 就是一个纯函数,将传入的组件重新包装,加入我们希望的功能,再返回这个新组件。

HOC 不应该修改传入组件,而应该使用组合的方式,通过将组件包装在容器组件中实现功能:

1
2
3
4
5
6
7
8
9
10
11
12
function logProps(WrappedComponent) {
return class extends React.Component {
componentDidUpdate(prevProps) {
console.log('Current props: ', this.props);
console.log('Previous props: ', prevProps);
}
render() {
// 将 input 组件包装在容器中,而不对其进行修改。Good!
return <WrappedComponent {...this.props} />;
}
}
}

在使用 HOC 的过程中需要遵守一些规定来让 HOC 更加的灵活和不易出错。

  • 将不相关的 props 传递给被包裹的组件

  • 最大化可组合性

  • 包装显示名称以便轻松调试

  • 不要在 render 方法中使用 HOC

  • Refs 不会被传递

深入 JSX

实际上,JSX 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖。如下 JSX 代码:

1
2
3
<MyButton color="blue" shadowSize={2}>
Click Me
</MyButton>

会编译为:

1
2
3
4
5
React.createElement(
MyButton,
{color: 'blue', shadowSize: 2},
'Click Me'
)

需要注意的是,上面这个是 js 代码,在运行的时候回去找 MyButton 这个对象,也就是我们定义的函数或者类,这个代码需要符合作用域规则。

在 JSX 类型中使用点语法

在 JSX 中,你也可以使用点语法来引用一个 React 组件。当你在一个模块中导出许多 React 组件时,这会非常方便。例如,如果 MyComponents.DatePicker 是一个组件,你可以在 JSX 中直接使用:

1
2
3
4
5
6
7
8
9
10
import React from 'react';

const MyComponents = {
DatePicker: function DatePicker(props) {
return <div>Imagine a {props.color} datepicker here.</div>;
}
}

function BlueDatePicker() {
return <MyComponents.DatePicker color="blue" />;}

用户定义的组件必须以大写字母开头

值得注意的是有一些 “falsy” 值,如数字 0,仍然会被 React 渲染。例如,以下代码并不会像你预期那样工作,因为当 props.messages 是空数组时,0 仍然会被渲染:

1
2
3
4
5
<div>
{props.messages.length &&
<MessageList messages={props.messages} />
}
</div>

要解决这个问题,确保 && 之前的表达式总是布尔值:

1
2
3
4
5
<div>
{props.messages.length > 0 &&
<MessageList messages={props.messages} />
}
</div>

反之,如果你想渲染 falsetruenullundefined 等值,你需要先将它们转换为字符串

Protals

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。通常我们使用这种方案来创建弹窗、对话框等

1
ReactDOM.createPortal(child, container)

第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。第二个参数(container)是一个 DOM 元素。

1
2
3
4
5
6
7
render() {
// React 并*没有*创建一个新的 div。它只是把子元素渲染到 `domNode` 中。
// `domNode` 是一个可以在任何位置的有效 DOM 节点。
return ReactDOM.createPortal(
this.props.children,
domNode );
}

事件冒泡

尽管 portal 可以被放置在 DOM 树中的任何地方,但在任何其他方面,其行为和普通的 React 子节点行为一致。由于 portal 仍存在于 React 树*, 且与 *DOM 树 中的位置无关,那么无论其子节点是否是 portal,像 context 这样的功能特性都是不变的。

官网实例 https://codepen.io/gaearon/pen/jGBWpE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// 在 DOM 中有两个容器是兄弟级 (siblings)
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');

class Modal extends React.Component {
constructor(props) {
super(props);
this.el = document.createElement('div');
}

componentDidMount() {
// 在 Modal 的所有子元素被挂载后,
// 这个 portal 元素会被嵌入到 DOM 树中,
// 这意味着子元素将被挂载到一个分离的 DOM 节点中。
// 如果要求子组件在挂载时可以立刻接入 DOM 树,
// 例如衡量一个 DOM 节点,
// 或者在后代节点中使用 ‘autoFocus’,
// 则需添加 state 到 Modal 中,
// 仅当 Modal 被插入 DOM 树中才能渲染子元素。
modalRoot.appendChild(this.el);
}

componentWillUnmount() {
modalRoot.removeChild(this.el);
}

render() {
return ReactDOM.createPortal(
this.props.children,
this.el
);
}
}

class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {clicks: 0};
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
// 当子元素里的按钮被点击时,
// 这个将会被触发更新父元素的 state,
// 即使这个按钮在 DOM 中不是直接关联的后代
this.setState(state => ({
clicks: state.clicks + 1
}));
}

render() {
return (
<div onClick={this.handleClick}>
<p>Number of clicks: {this.state.clicks}</p>
<p>
Open up the browser DevTools
to observe that the button
is not a child of the div
with the onClick handler.
</p>
<Modal>
<Child />
</Modal>
</div>
);
}
}

function Child() {
// 这个按钮的点击事件会冒泡到父元素
// 因为这里没有定义 'onClick' 属性
return (
<div className="modal">
<button>Click</button>
</div>
);
}

ReactDOM.render(<Parent />, appRoot);

只看这个代码也行,在 Codepen 中的实例需要注意的是这个 button 并不是弹出来的,就是一个普通的居中按钮,通过调节背景色来的,这也是为什么点 button 以外的地方依然会 +1。

大概最后渲染出来的结构就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div id="root">
<div id="app-root">
<div onClick="{this.handleClick}">
<p>Number of clicks: {this.state.clicks}</p>
<p>
Open up the browser DevTools to observe that the button is not a child
of the div with the onClick handler.
</p>
<Modal>
<Child />
</Modal>
</div>
</div>
<div id="modal-root">
<div>
<div className="modal">
<button>Click</button>
</div>
</div>
</div>
</div>

这个实例最关键的地方在于,DOM 结构下,button 的父元素是 modal-root ,但是在点击时会冒泡到 app-root 中。

在父组件里捕获一个来自 portal 冒泡上来的事件,使之能够在开发时具有不完全依赖于 portal 的更为灵活的抽象。例如,如果你在渲染一个 <Modal /> 组件,无论其是否采用 portal 实现,父组件都能够捕获其事件。

也就是说父组件是一定可以捕获到子组件的事件的。所以在处理 protal 的事件的时候,只需要在父组件中处理就可以了。

Profiler

这个 API 的主要功能是提供性能检测

Profiler 测量渲染一个 React 应用多久渲染一次以及渲染一次的“代价”。 它的目的是识别出应用中渲染较慢的部分,或是可以使用类似 memoization 优化的部分,并从相关优化中获益。

不使用 ES6

大部分情况下使用 ES6 是最好的方式,如果条件限制的话。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Greeting extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
// 如果你还未使用过 ES6,你可以使用 create-react-class 模块:

var createReactClass = require('create-react-class');
var Greeting = createReactClass({ // 这里用的是一个对象作为参数,键名是render,键值是一个函数。
render: function() {
return <h1>Hello, {this.props.name}</h1>;
}
});
// ES6 中的 class 与 createReactClass() 方法十分相似,但有以下几个区别值得注意。

不使用 JSX 的 React

jsx 相当于是 React 设立的一个语法糖,本质上是一个 jsx 对象。

例如,用 JSX 编写的代码:

1
2
3
4
5
6
7
8
9
10
class Hello extends React.Component {
render() {
return <div>Hello {this.props.toWhat}</div>;
}
}

ReactDOM.render(
<Hello toWhat="World" />,
document.getElementById('root')
);

可以编写为不使用 JSX 的代码:

1
2
3
4
5
6
7
8
9
10
class Hello extends React.Component {
render() {
return React.createElement('div', null, `Hello ${this.props.toWhat}`);
}
}

ReactDOM.render(
React.createElement(Hello, {toWhat: 'World'}, null),
document.getElementById('root')
);

Refs and the DOM

在 Vue 文档中是这么定义 Refs 的

尽管存在 prop 和事件,有的时候你仍可能需要在 JavaScript 里直接访问一个子组件。为了达到这个目的,你可以通过 ref 这个 attribute 为子组件赋予一个 ID 引用。例如:

1
2
> <base-input ref="usernameInput"></base-input>
>

现在在你已经定义了这个 ref 的组件里,你可以使用:

1
2
> this.$refs.usernameInput
>

来访问这个 <base-input> 实例,以便不时之需。

简单说就是给某个子组件或者元素添加一个全局ID,和普通的 class id 不同,refs 的作用范围是全局的,并不是仅在某个组件内。并且可以跨组件去访问到。

在 React 中也是类似的思想。只是文档说的不是很明白,我就不抄这段了。

Refs 是使用 React.createRef() 创建的,并通过 ref 属性附加到 React 元素。在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们。

1
2
3
4
5
6
7
8
9
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}

访问 Refs

当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中被访问。

1
const node = this.myRef.current;

ref 的值根据节点的类型而有所不同:

  • ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。
  • ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。
  • 你不能在函数组件上使用 ref 属性,因为他们没有实例。

Render Props

具有 render prop 的组件接受一个函数,该函数返回一个 React 元素并调用它而不是实现自己的渲染逻辑。

1
2
3
<DataProvider render={data => (
<h1>Hello {data.target}</h1>
)}/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Cat extends React.Component {
render() {
const mouse = this.props.mouse;
return (
<img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
);
}
}

class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}

handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}

render() {
return (
<div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>

{/*
Instead of providing a static representation of what <Mouse> renders,
use the `render` prop to dynamically determine what to render.
*/}
{this.props.render(this.state)}
</div>
);
}
}

class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>移动鼠标!</h1>
<Mouse render={mouse => (
<Cat mouse={mouse} />
)}/>
</div>
);
}
}

文档 挺长的,主要还是上面这段代码,也是 Render Props 的主要用法。

先来梳理一下结构关系,实际使用的组件是已封装好的 MouseTracker 组件,包含 MouseCat 两个子组件,Cat 是通过 Mouse 组件中的 render={} 属性进行组合的。

Cat 组件需要使用 mouse 参数,Mouse 组件的 render 属性是一个匿名函数,参数是 mouse。在 Mouse 组件的 render() 函数中有一个 this.props.render(this.state) ,也就是替代了在 MouseTracker 组件中的 render 属性。并且 this.state 作为参数。

通过这种方法可以让 Mouse 组件动态决定渲染内容。

Render prop 是一个用于告知组件需要渲染什么内容的函数 prop。

一个很重要的点是,这里的 Render 并不是组件内的 render 函数,而是一个 prop ,属性,所以用其他的命名也是可以的。

1
2
3
<Mouse children={mouse => (
<p>鼠标的位置是 {mouse.x},{mouse.y}</p>
)}/>

记住,children prop 并不真正需要添加到 JSX 元素的 “attributes” 列表中。相反,你可以直接放置到元素的内部

1
2
3
4
5
<Mouse>
{mouse => (
<p>鼠标的位置是 {mouse.x},{mouse.y}</p>
)}
</Mouse>

小结

react 的 api 参考https://react.docschina.org/docs/react-api.html

钩子

一直以来,我都认为钩子是个很高级的东西,但是其实没那么高级,但是他确实好用。不管是 Vue 还是 React,生命周期函数都是属于钩子的一种。但是 Vue 并没有提供其他的钩子函数以及自定义 Hook。

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

已有的钩子 API 参考 https://react.docschina.org/docs/hooks-reference.html

在钩子之前,复用组件状态是通过高阶组件(HOC)和 RenderProps 完成的。

提取自定义 Hook

当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以也同样适用这种方式。

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。 例如,下面的 useFriendStatus 是我们第一个自定义的 Hook:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);

useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}

ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});

return isOnline;
}

Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。

Redux

可以看这个 https://www.redux.org.cn/

Router

感觉看这个就够了 https://reactrouter.com/web/example/basic 这网站做的太好了,我哭了都。不知道比那些指南什么的高到哪里去了。

第一,代码很清晰的呈现给你去研究。

第二,各式功能都介绍了并且有代码。

第三,注释很完整,看不懂代码的话可以先看对应的注释了解结构。

下面的文章是对代码的深一步注释和解读

Basic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from "react-router-dom";

// This site has 3 pages, all of which are rendered
// dynamically in the browser (not server rendered).
//
// Although the page does not ever refresh, notice how
// React Router keeps the URL up to date as you navigate
// through the site. This preserves the browser history,
// making sure things like the back button and bookmarks
// work properly.

export default function BasicExample() {
return (
<Router>
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/dashboard">Dashboard</Link>
</li>
</ul>
<hr />
{/*
A <Switch> looks through all its children <Route>
elements and renders the first one whose path
matches the current URL. Use a <Switch> any time
you have multiple routes, but you want only one
of them to render at a time
通过 Switch 组件去控制不同路由下的内容。
*/}
<Switch>
{/*
可以看出每一个路由的结构是由 Route 组件和页面所对应的组件组成的。
*/}
<Route exact path="/">
<Home />
</Route>
<Route path="/about">
<About />
</Route>
<Route path="/dashboard">
<Dashboard />
</Route>
</Switch>
</div>
</Router>
);
}

// You can think of these components as "pages"
// in your app.
// 每一个组件都可以看做是一个单独的页面。
function Home() {
return (
<div>
<h2>Home</h2>
</div>
);
}

function About() {
return (
<div>
<h2>About</h2>
</div>
);
}

function Dashboard() {
return (
<div>
<h2>Dashboard</h2>
</div>
);
}

也就是说你如果需要写另外一个页面的话,直接写一个组件就可以了。如果不需要公共组件或者跳转连接的话可以直接在 App 里面用 Switch 吧。

URL Parameters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
useParams
} from "react-router-dom";

// Params are placeholders in the URL that begin
// with a colon, like the `:id` param defined in
// the route in this example. A similar convention
// is used for matching dynamic segments in other
// popular web frameworks like Rails and Express.

export default function ParamsExample() {
return (
<Router>
<div>
<h2>Accounts</h2>

<ul>
<li>
<Link to="/netflix">Netflix</Link>
</li>
<li>
<Link to="/zillow-group">Zillow Group</Link>
</li>
<li>
<Link to="/yahoo">Yahoo</Link>
</li>
<li>
<Link to="/modus-create">Modus Create</Link>
</li>
</ul>

<Switch>
{/*
在path路径中通过 : + 变量名 的方式设置路由变量。这种方式会让参数在 URL 中体现。
可以通过 children 属性而非嵌套的方式设置路由指向的组件或页面。
*/}
<Route path="/:id" children={<Child />} />
</Switch>
</div>
</Router>
);
}

function Child() {
// We can use the `useParams` hook here to access
// the dynamic pieces of the URL.
let { id } = useParams();

return (
<div>
<h3>ID: {id}</h3>
</div>
);
}

在 API 介绍中可以找到 https://reactrouter.com/web/api/Route/children-func

Sometimes you need to render whether the path matches the location or not. In these cases, you can use the function children prop. It works exactly like render except that it gets called whether there is a match or not.

The children render prop receives all the same route props as the component and render methods, except when a route fails to match the URL, then match is null. This allows you to dynamically adjust your UI based on whether or not the route matches. Here we’re adding an active class if the route matches

正常情况下指定路由对应的组件是通过嵌套或者 component 属性

1
2
3
4
5
6
7
8
9
> <Route
> path={to}
> children={({ match }) => (
> <li className={match ? "active" : ""}>
> <Link to={to} {...rest} />
> </li>
> )}
> />
>

但是某些时候可以使用 children 来使得组件更加可控,回调函数中的 match 是一个 Boolean 代表 URL 是否匹配

Nesting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
useParams,
useRouteMatch
} from "react-router-dom";

// Since routes are regular React components, they
// may be rendered anywhere in the app, including in
// child elements.
//
// This helps when it's time to code-split your app
// into multiple bundles because code-splitting a
// React Router app is the same as code-splitting
// any other React app.

export default function NestingExample() {
return (
<Router>
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/topics">Topics</Link>
</li>
</ul>
<hr />

<Switch>
<Route exact path="/">
{/*
使用了一个新的属性,如果设置 exact 属性,匹配的就不是 path 而是 location.pathname
一般来说 / 可以匹配 / 也可以匹配 /topics,而且由于这个路由写的靠前,所以是默认匹配 /
所以在嵌套路由的使用中,需要在URL有重复部分的路由的第一个标明 exact 属性。
*/}
<Home />
</Route>
<Route path="/topics">
<Topics />
</Route>
</Switch>
</div>
</Router>
);
}

function Home() {
return (
<div>
<h2>Home</h2>
</div>
);
}

function Topics() {
// The `path` lets us build <Route> paths that are
// relative to the parent route, while the `url` lets
// us build relative links.
let { path, url } = useRouteMatch();
// 这两个一般来说值是一样的,都是指上一层的路由值。
// /topics
console.log(path,url)
return (
<div>
<h2>Topics</h2>
<ul>
<li>
<Link to={`${url}/rendering`}>Rendering with React</Link>
</li>
<li>
<Link to={`${url}/components`}>Components</Link>
</li>
<li>
<Link to={`${url}/props-v-state`}>Props v. State</Link>
</li>
</ul>

<Switch>
<Route exact path={path}>
<h3>Please select a topic.</h3>
</Route>
<Route path={`${path}/:topicId`}>
<Topic />
</Route>
</Switch>
</div>
);
}

function Topic() {
// The <Route> that rendered this component has a
// path of `/topics/:topicId`. The `:topicId` portion
// of the URL indicates a placeholder that we can
// get from `useParams()`.
let { topicId } = useParams();

return (
<div>
<h3>{topicId}</h3>
</div>
);
}

Redirect(Auth)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
Redirect,
useHistory,
useLocation
} from "react-router-dom";

// This example has 3 pages: a public page, a protected
// page, and a login screen. In order to see the protected
// page, you must first login. Pretty standard stuff.
//
// First, visit the public page. Then, visit the protected
// page. You're not yet logged in, so you are redirected
// to the login page. After you login, you are redirected
// back to the protected page.
//
// Notice the URL change each time. If you click the back
// button at this point, would you expect to go back to the
// login page? No! You're already logged in. Try it out,
// and you'll see you go back to the page you visited
// just *before* logging in, the public page.

export default function AuthExample() {
return (
<Router>
<div>
<AuthButton />

<ul>
<li>
<Link to="/public">Public Page</Link>
</li>
<li>
<Link to="/protected">Protected Page</Link>
</li>
</ul>

<Switch>
<Route path="/public">
<PublicPage />
</Route>
<Route path="/login">
<LoginPage />
</Route>
<PrivateRoute path="/protected">
<ProtectedPage />
</PrivateRoute>
</Switch>
</div>
</Router>
);
}

const fakeAuth = {
isAuthenticated: false,
authenticate(cb) {
fakeAuth.isAuthenticated = true;
setTimeout(cb, 100); // fake async
},
signout(cb) {
fakeAuth.isAuthenticated = false;
setTimeout(cb, 100);
}
};

function AuthButton() {
let history = useHistory();
// 通过三元操作符鉴定权限
return fakeAuth.isAuthenticated ? (
<p>
Welcome!{" "}
<button
onClick={() => {
fakeAuth.signout(() => history.push("/"));
}}
>
Sign out
</button>
</p>
) : (
<p>You are not logged in.</p>
);
}

// A wrapper for <Route> that redirects to the login
// screen if you're not yet authenticated.
function PrivateRoute({ children, ...rest }) {
// children 是路由组件下的所有子组件,rest 是路由信息
// rest Object {path: "/protected", location: Object, computedMatch: Object}
return (
<Route
{...rest}
render={({ location }) =>
fakeAuth.isAuthenticated ? (
children
) : (
<!-- 如果没有权限会重定向到 /login 路由
我怎么看不懂呢,怎么就渲染 LoginPage 了呢 -->
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)
}
/>
);
}

function PublicPage() {
return <h3>Public</h3>;
}

function ProtectedPage() {
return <h3>Protected</h3>;
}

function LoginPage() {
let history = useHistory();
let location = useLocation();

let { from } = location.state || { from: { pathname: "/" } };
let login = () => {
fakeAuth.authenticate(() => {
history.replace(from);
});
};

return (
<div>
<p>You must log in to view the page at {from.pathname}</p>
<button onClick={login}>Log in</button>
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
useRouteMatch
} from "react-router-dom";

// This example show how you could create a custom
// <Link> that renders something special when the URL
// is the same as the one the <Link> points to.

export default function CustomLinkExample() {
return (
<Router>
<div>
<OldSchoolMenuLink
activeOnlyWhenExact={true}
to="/"
label="Home"
/>
<OldSchoolMenuLink to="/about" label="About" />

<hr />

<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/about">
<About />
</Route>
</Switch>
</div>
</Router>
);
}

// 等于function(){const {label,to,activeOnlyWhenExact}=props}
function OldSchoolMenuLink({ label, to, activeOnlyWhenExact }) {
// 通过 useRouteMatch 来判断路由条件是否符合,通过传入的参数,生成不同的效果。
let match = useRouteMatch({
path: to,
exact: activeOnlyWhenExact
});

return (
<div className={match ? "active" : ""}>
{match && "> "}
<Link to={to}>{label}</Link>
</div>
);
}

function Home() {
return (
<div>
<h2>Home</h2>
</div>
);
}

function About() {
return (
<div>
<h2>About</h2>
</div>
);
}

Preventing Transitions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import React, { useState } from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
Prompt
} from "react-router-dom";
// 这里使用了 Prompt 组件,是一个弹窗式的组件,这个组件并不会显式的显示出来,而是作为表单提交前的一个确认,确认即会提交表单,取消就不会提交表单。

// Sometimes you want to prevent the user from
// navigating away from a page. The most common
// use case is when they have entered some data
// into a form but haven't submitted it yet, and
// you don't want them to lose it.

export default function PreventingTransitionsExample() {
return (
<Router>
<ul>
<li>
<Link to="/">Form</Link>
</li>
<li>
<Link to="/one">One</Link>
</li>
<li>
<Link to="/two">Two</Link>
</li>
</ul>

<Switch>
<Route path="/" exact children={<BlockingForm />} />
<Route path="/one" children={<h3>One</h3>} />
<Route path="/two" children={<h3>Two</h3>} />
</Switch>
</Router>
);
}

function BlockingForm() {
let [isBlocking, setIsBlocking] = useState(false);

return (
<form
onSubmit={event => {
event.preventDefault();
event.target.reset();
setIsBlocking(false);
}}
>
<Prompt
when={isBlocking}
message={location =>
`Are you sure you want to go to ${location.pathname}`
}
/>

<p>
Blocking?{" "}
{isBlocking ? "Yes, click a link or the back button" : "Nope"}
</p>

<p>
<input
size="50"
placeholder="type something to block transitions"
onChange={event => {
setIsBlocking(event.target.value.length > 0);
}}
/>
</p>

<p>
<button>Submit to stop blocking</button>
</p>
</form>
);
}

Not Match(404)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import React from "react";
import {
BrowserRouter as Router,
Route,
Link,
Switch,
Redirect,
useLocation
} from "react-router-dom";

// You can use the last <Route> in a <Switch> as a kind of
// "fallback" route, to catch 404 errors.
//
// There are a few useful things to note about this example:
//
// - A <Switch> renders the first child <Route> that matches
// - A <Redirect> may be used to redirect old URLs to new ones
// - A <Route path="*> always matches

export default function NoMatchExample() {
return (
<Router>
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/old-match">Old Match, to be redirected</Link>
</li>
<li>
<Link to="/will-match">Will Match</Link>
</li>
<li>
<Link to="/will-not-match">Will Not Match</Link>
</li>
<li>
<Link to="/also/will/not/match">Also Will Not Match</Link>
</li>
</ul>

<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/old-match">
<Redirect to="/will-match" />
</Route>
<Route path="/will-match">
<WillMatch />
</Route>
<!-- 通过 * 匹配所有的路径,由于是在最后一个,所以会匹配所有值钱没有匹配的路径。 -->
<Route path="*">
<NoMatch />
</Route>
</Switch>
</div>
</Router>
);
}

function Home() {
return <h3>Home</h3>;
}

function WillMatch() {
return <h3>Matched!</h3>;
}

function NoMatch() {
let location = useLocation();

return (
<div>
<h3>
No match for <code>{location.pathname}</code>
</h3>
</div>
);
}

Recursive Paths

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
Redirect,
useParams,
useRouteMatch
} from "react-router-dom";

// Sometimes you don't know all the possible routes
// for your application up front; for example, when
// building a file-system browsing UI or determining
// URLs dynamically based on data. In these situations,
// it helps to have a dynamic router that is able
// to generate routes as needed at runtime.
//
// This example lets you drill down into a friends
// list recursively, viewing each user's friend list
// along the way. As you drill down, notice each segment
// being added to the URL. You can copy/paste this link
// to someone else and they will see the same UI.
//
// Then click the back button and watch the last
// segment of the URL disappear along with the last
// friend list.

export default function RecursiveExample() {
return (
<Router>
<Switch>
<Route path="/:id">
<Person />
</Route>
<Route path="/">
<Redirect to="/0" />
</Route>
<!-- 这里由于 / 的路由在 /:id 下面,所以会优先匹配 /:id ,当然也就会匹配 /:id/:id/:id 这种情况下就会出现无限的path的情况 -->
</Switch>
</Router>
);
}

function Person() {
let { url } = useRouteMatch();
let { id } = useParams();
let person = find(parseInt(id));

return (
<div>
<h3>{person.name}’s Friends</h3>

<ul>
{person.friends.map(id => (
<li key={id}>
<Link to={`${url}/${id}`}>{find(id).name}</Link>
</li>
))}
</ul>

<Switch>
<Route path={`${url}/:id`}>
<Person />
</Route>
</Switch>
</div>
);
}

const PEEPS = [
{ id: 0, name: "Michelle", friends: [1, 2, 3] },
{ id: 1, name: "Sean", friends: [0, 3] },
{ id: 2, name: "Kim", friends: [0, 1, 3] },
{ id: 3, name: "David", friends: [1, 2] }
];

function find(id) {
return PEEPS.find(p => p.id === id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from "react-router-dom";

// Each logical "route" has two components, one for
// the sidebar and one for the main area. We want to
// render both of them in different places when the
// path matches the current URL.

// We are going to use this route config in 2
// spots: once for the sidebar and once in the main
// content section. All routes are in the same
// order they would appear in a <Switch>.
// 这种写法可以更加直观的看到本页的一个路由情况。
const routes = [
{
path: "/",
exact: true,
sidebar: () => <div>home!</div>,
main: () => <h2>Home</h2>
},
{
path: "/bubblegum",
sidebar: () => <div>bubblegum!</div>,
main: () => <h2>Bubblegum</h2>
},
{
path: "/shoelaces",
sidebar: () => <div>shoelaces!</div>,
main: () => <h2>Shoelaces</h2>
}
];

export default function SidebarExample() {
return (
<Router>
<div style={{ display: "flex" }}>
<div
style={{
padding: "10px",
width: "40%",
background: "#f0f0f0"
}}
>
<ul style={{ listStyleType: "none", padding: 0 }}>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/bubblegum">Bubblegum</Link>
</li>
<li>
<Link to="/shoelaces">Shoelaces</Link>
</li>
</ul>

<Switch>
{routes.map((route, index) => (
// You can render a <Route> in as many places
// as you want in your app. It will render along
// with any other <Route>s that also match the URL.
// So, a sidebar or breadcrumbs or anything else
// that requires you to render multiple things
// in multiple places at the same URL is nothing
// more than multiple <Route>s.
<Route
key={index}
path={route.path}
exact={route.exact}
children={<route.sidebar />}
/>
))}
</Switch>
</div>

<div style={{ flex: 1, padding: "10px" }}>
<Switch>
{routes.map((route, index) => (
// Render more <Route>s with the same paths as
// above, but different components this time.
<Route
key={index}
path={route.path}
exact={route.exact}
children={<route.main />}
/>
))}
</Switch>
</div>
</div>
</Router>
);
}

感觉这个 SideBar 的示例没什么意义啊,就是加了 style 而已。只是提供一个比较直观的路由的方式。

Animated Transitions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import "./packages/react-router-dom/examples/Animation/styles.css";

import React from "react";
import {
TransitionGroup,
CSSTransition
} from "react-transition-group";
// 这两个组件是react的过渡组件,详情查看 https://reactcommunity.org/react-transition-group/ 可以提供默认的动画效果。
import {
BrowserRouter as Router,
Switch,
Route,
Link,
Redirect,
useLocation,
useParams
} from "react-router-dom";

export default function AnimationExample() {
return (
<Router>
<Switch>
<Route exact path="/">
<Redirect to="/hsl/10/90/50" />
</Route>
<Route path="*">
<AnimationApp />
</Route>
</Switch>
</Router>
);
}

function AnimationApp() {
let location = useLocation();

return (
<div style={styles.fill}>
<ul style={styles.nav}>
<NavLink to="/hsl/10/90/50">Red</NavLink>
<NavLink to="/hsl/120/100/40">Green</NavLink>
<NavLink to="/rgb/33/150/243">Blue</NavLink>
<NavLink to="/rgb/240/98/146">Pink</NavLink>
</ul>

<div style={styles.content}>
<TransitionGroup>
{/*
This is no different than other usage of
<CSSTransition>, just make sure to pass
`location` to `Switch` so it can match
the old location as it animates out.
*/}
<CSSTransition
key={location.key}
classNames="fade"
timeout={300}
>
<Switch location={location}>
<Route path="/hsl/:h/:s/:l" children={<HSL />} />
<Route path="/rgb/:r/:g/:b" children={<RGB />} />
</Switch>
</CSSTransition>
</TransitionGroup>
</div>
</div>
);
}

function NavLink(props) {
return (
<li style={styles.navItem}>
<Link {...props} style={{ color: "inherit" }} />
</li>
);
}

function HSL() {
let { h, s, l } = useParams();

return (
<div
style={{
...styles.fill,
...styles.hsl,
background: `hsl(${h}, ${s}%, ${l}%)`
}}
>
hsl({h}, {s}%, {l}%)
</div>
);
}

function RGB() {
let { r, g, b } = useParams();

return (
<div
style={{
...styles.fill,
...styles.rgb,
background: `rgb(${r}, ${g}, ${b})`
}}
>
rgb({r}, {g}, {b})
</div>
);
}

const styles = {};

styles.fill = {
position: "absolute",
left: 0,
right: 0,
top: 0,
bottom: 0
};

styles.content = {
...styles.fill,
top: "40px",
textAlign: "center"
};

styles.nav = {
padding: 0,
margin: 0,
position: "absolute",
top: 0,
height: "40px",
width: "100%",
display: "flex"
};

styles.navItem = {
textAlign: "center",
flex: 1,
listStyleType: "none",
padding: "10px"
};

styles.hsl = {
...styles.fill,
color: "white",
paddingTop: "20px",
fontSize: "30px"
};

styles.rgb = {
...styles.fill,
color: "white",
paddingTop: "20px",
fontSize: "30px"
};

这个组件通过对象的方式设置 css

1
2
3
4
5
style={{
...styles.fill,
...styles.hsl,
background: `hsl(${h}, ${s}%, ${l}%)`
}}

Route Config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from "react-router-dom";

// Some folks find value in a centralized route config.
// A route config is just data. React is great at mapping
// data into components, and <Route> is a component.
// 翻译一下好了,意思大概就是路由配置只是 data 而已,通过 map 函数生成路由。
// Our route config is just an array of logical "routes"
// with `path` and `component` props, ordered the same
// way you'd do inside a `<Switch>`.
const routes = [
{
path: "/sandwiches",
component: Sandwiches
},
{
path: "/tacos",
component: Tacos,
routes: [
{
path: "/tacos/bus",
component: Bus
},
{
path: "/tacos/cart",
component: Cart
}
]
}
];

export default function RouteConfigExample() {
return (
<Router>
<div>
<ul>
<li>
<Link to="/tacos">Tacos</Link>
</li>
<li>
<Link to="/sandwiches">Sandwiches</Link>
</li>
</ul>

<Switch>
{routes.map((route, i) => (
<!-- ...route 出来就是 path: "/sandwiches" component: Sandwiches
当然也可以用 path={route.path} component={route.component} 这样写,不过不太清除里面的性能差异 -->
<RouteWithSubRoutes key={i} {...route} />
))}
</Switch>
</div>
</Router>
);
}

// A special wrapper for <Route> that knows how to
// handle "sub"-routes by passing them in a `routes`
// prop to the component it renders.
function RouteWithSubRoutes(route) {
return (
<Route
path={route.path}
render={props => (
// pass the sub-routes down to keep nesting
<route.component {...props} routes={route.routes} />
)}
/>
);
}

function Sandwiches() {
return <h2>Sandwiches</h2>;
}

function Tacos({ routes }) {
return (
<div>
<h2>Tacos</h2>
<ul>
<li>
<Link to="/tacos/bus">Bus</Link>
</li>
<li>
<Link to="/tacos/cart">Cart</Link>
</li>
</ul>

<Switch>
{routes.map((route, i) => (
<RouteWithSubRoutes key={i} {...route} />
))}
</Switch>
</div>
);
}

function Bus() {
return <h3>Bus</h3>;
}

function Cart() {
return <h3>Cart</h3>;
}