我看的是 Learn React - ScrimbaAdvanced React - Scrimba

React 入门

Intro 介绍

CDN、导包

CDN:

1
2
3
4
5
6
7
8
9
<script
crossorigin
src="https://unpkg.com/react@17/umd/react.development.js"
/>
<script
crossorigin
src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"
></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

导包,无需使用 CDN:

1
2
import React from "react";
import ReactDOM from "react-dom";

特点

  1. Composable 可组合的

    我们有一些小的代码碎片,我们可以把它们拼在一起,组成更大的东西。

  2. Declarative 声明的

    声明性意味着我可以告诉计算机要做什么,并期望它处理细节。

渲染

ReactDOM.render(<html代码>,<在哪渲染>)

例如:

1
2
3
4
import React from "react";
import ReactDOM from "react-dom";

ReactDOM.render(<h1>Harris</h1>, document.getElementById("root"));

或者(React 18 新语法)

1
2
3
ReactDOM.createRoot(<在哪渲染>).render(
<代码>
)

例如:

1
2
3
4
import React from "react";
import ReactDOM from "react-dom/client";

ReactDOM.createRoot(document.getElementById("root")).render(<h1>Harris</h1>);

如果将 <html 代码> 定义到变量里,最外层只能是父节点一个,可以是<div> ... </div>也可以写成空标签<> ... </>

1
2
3
4
5
const page = (
<div>
<html代码>
</div>
)

JSX 内部的 JS

括号() 里写 HTML 代码

花括号{} 里的代码可以写 JS 语法

1
2
3
4
5
6
7
8
9
function App() {
const firstName = "Harris"
const lastName = "Wong"
return (
<h1>Hello {firstName} {lastName}!</h1>
)
}

// Hello Harris Wong!

Note: 如果想在 HTML 里加入 JS 逻辑代码,可以加上花括号{},但只能写表达式,不能写赋值等语句。

Component: function

Custom Components 自定义组件

1
2
3
4
5
6
7
8
9
10
import React from "react"
import ReactDOM from "react-dom"

function TemporaryName() {
return (
<html代码>
)
}

ReactDOM.render(<TemporaryName />, document.getElementById("root"))

更简洁的写法

1
2
3
const TemporaryName = () => (
<html代码>
)

Note:组件名要大写

什么是 React 组件?

返回 React 元素 的函数。(UI)

Parent/Child Components 父子组件

形如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Children() {
return (
<html代码>
)
}

function Father() {
return (
<div>
<Children />
<html代码>
</div>
)
}

Styling with Classes 修饰类

类名不再是像 JS 里的 class 了,而是 className

1
<h1 className="name">Harris</h1>

把 JSX 里的代码当做 HTML,CSS 代码直接在 .css 文件里写,和往常一样

Organize components 整理组件

除了将代码按功能或者区域拆分成许多代码块还不够,代码量更大的时候,会考虑分离出一个文件出来。那么这时如何创建“组件文件”呢?且如何引用组件呢?

“组件文件”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from "react"

export default function Header() {
<<<<<<< HEAD
return (<html代码>)
=======
return (<代码>)
>>>>>>> 345954b736d625ffb8d340a541b4888b28ad6077
}

// 或者

import React from "react"

function Header() {
return (<html代码>)
}

export default Header

引用组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from "react"
import ReactDOM from "react-dom"
import Header from "./Header" (可以不带上后缀.js)

function Page() {
return (
<div>
<Header />
</div>
)
}

ReactDOM.render(<Page />, document.getElementById("root"))

Props 属性

为了完成组件之间数据的通信,引入了 Props 概念

"Props" refers to the properties being passed into a component in order for it to work correctly, similar to how a function receives parameters: "from above." A component receiving props is not allowed to modify those props. (l.e. they are "immutable(不可变的)")

例如,下面是一个加法器案例:

1
2
3
4
5
import React from "react"

export default function Addition(props) {
return {props.a} + {props.b}
}
1
<Addition a="1" b="2" />

可以看到,引用组件的同时,a、b 属性值将被传到 Addition 函数里的 props 参数里,并以对象的方式保存。通过 props.<属性名>可以在函数里调用组件传来的值。

  1. 组件和函数里的 props 名称可以随意取,但函数里的参数最好还是写 props。

  2. img 里的 src 可以像这样调用数据:

    1
    2
    3
    <img src={props.img}/>
    或类似于
    <img src={`../images/${props.img}`} />

    ${}这个符号能使我们在字符串里传入变量

  3. 原生 HTML 标签的 style 可以这样调用数据:<p style={{display: props.name ? "block" : "none"}}>{props.name}</p>。这里注意是 2 个花括号,因为里面包含 JS 里的三元表达式(ternary expression),所以要再加一个花括号,当然了,style 里的值本身就是一个对象,所以需要花括号。

Props 的作用

What do props help us accomplish?

Make a component more reusable.

另一种 props 取值方式

1
2
3
4
5
6
7
8
export default function Contact({title, content} {
return (
<div>
<h1>{title}</h1>
<p>{content}</p>
</div>
)
}

Passing in non-string props 传入非字符串类的 Props

1
2
3
4
5
6
<Student
name="Harris"
DOB={2000.08}
isMale={true}
grade={{ Chinese: 88, Math: 96, English: 98 }}
/>

由此可见,传入非字符串需要用到花括号 {}

prop: key

记得组件里得带上一个 key 属性,值一般是 id 之类唯一的值,否则控制台会有一个 warning

推荐使用 nanoid 包,会随机产生一个 id

Pass object as props

这样不需要在调用组件时写太多数据传输的相关代码

1
2
3
const students = data.map((item) => {
return <Students key={item.id} item={item} />;
});
1
2
3
export default function Students(props) {
//用props.item来调用
}

Spread object as props 将对象作为 props 展开

这样写代码更简洁

1
2
3
const students = data.map((item) => {
return <Students key={item.id} {...item} />;
});
1
2
3
4
5
6
7
export default function Students(props) {
<<<<<<< HEAD
//还是用props来调用,但相比上一个例子,可少写props后面的“.item”
=======
//还是用props来调用
>>>>>>> 345954b736d625ffb8d340a541b4888b28ad6077
}

对比一下之前的繁杂写法:

1
2
3
4
5
6
7
8
9
10
11
const students = data.map(item => {
return (
<Students
key={item.id}
name={item.name}
age={item.age}
sex={item.sex}
...
/>
)
})

props.children

除了下面这种写法可以传值

<Students name="Harris", sex="Male">

还有如下方式,可以将 html 代码通过 props.children传入组件里

1
2
3
4
<Students>
<h1>Harris</h1>
<h1>Male</h1>
</Students>
1
2
3
export default function Students(props) {
return <div>{props.children}</div>;
}

.map()

一般这里使用.map()是为了批量将数据加工成为可视化代码,例如:

1
2
3
const students = studentData.map((student) => {
return <Student name={student.name} sex={student.sex} />;
});

小问答

  1. What does the .map() array method do?
    Returns a new array. Whatever gets returned from the callback function provided is placed at the same index in the new array. Usually, we take the items from the original array and modify them in some way.

  2. What do we usually use .map() for in React?
    Convert an array of raw data into an array of JSX elements that can be displayed on the page.

  3. Why is using .map() better than just creating the components manually by typing them out?
    It makes our code more "self-sustaining" - not requiring additional changes whenever the data changes.

Static Web Pages 静态 Web 页面

  • Read-only, no changes to data
  • Examples
    • Blogs
    • News sites
    • etc.

Dynamic Web Apps 动态 Web 应用程序

  • Read-write: ability to change data
  • Highly interactive
  • Displays your data

Event listeners 事件侦听器

An event listener is a procedure in JavaScript that waits for an event to occur. A simple example of an event is a user clicking the mouse or pressing a key on the keyboard.

1
2
3
4
5
6
7
8
9
10
11
export default function App() {
function handleClick() {
console.log("I was clicked!");
}

return (
<>
<button onClick={handleClick}>Click me</button>
</>
);
}

更多:Mouse Events 鼠标事件(95%的事件侦听事件都和 鼠标事件 有关)

State 状态

我们编码时发现 function 运行后无法更改组件里的变量,这时需引入 state 概念。

"State" refers to values that are managed by the component, similar to variables declared inside a function. Any time you have changing values that should be save/displayed, you'll likely be using state.

1
2
3
4
5
6
7
8
9
const isMale = React.useState("Yes");
return (
<div className="state">
<div className="state--value">
<h1>{isMale[0]}</h1>
</div>
</div>
);
// isMale打印后:{"Yes", f()}

f()指的是一个 function,通过这个 function 咋们可以改变 state 的值

用 Array destructuring 数组解构这种方式:

1
2
3
4
5
6
7
8
const [isMale, func] = React.useState("Yes");
return (
<div className="state">
<div className="state--value">
<h1>{isMale}</h1>
</div>
</div>
);

注意:是[isMale, func]而不是{isMale, func},中括号[ ]!

Changing state

1
2
3
4
5
6
7
8
9
10
11
12
13
const [isMale, setIsMale] = React.useState("Yes");

function handleClick() {
setIsMale("No");
}

return (
<div className="state">
<div className="state--value" onClick={handleClick}>
<h1>{isMale}</h1>
</div>
</div>
);

Callback function 回调函数

If you ever need the old value of state to help you determine the new value of state, you should pass a callback function to your state setter function instead of using state directly. This callback function will receive the old value of state as its parameter, which you can then use to determine your new value of state.

言简意赅,你若想更改 state 值,建议采用回调函数

以加法函数为例,采用回调函数:

1
2
3
function add() {
setCount((prevCount) => prevCount + 1);
}

Complex state: arrays 数组

改数组有些麻烦

以 To-do list 为例,这用到了 ES6 新语法:

1
2
3
4
5
6
7
8
const [thingsArray, setThingsArray] = React.useState(["Thing 1", "Thing 2"]);

function addItem() {
setThingsArray((prevThingsArray) => [
...prevThingsArray,
`Thing ${prevThingsArray.length}`,
]);
}

Complex state: objects 对象

以学生信息为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const [stuInfo, setStuInfo] = React.useState({
firstName: "Harris",
lastName: "Wong",
isMale: true,
});

function toggleIsMale() {
setContact((prevStuInfo) => {
return {
...prevStuInfo,
isMale: !prevStuInfo.isMale,
};
});
}
//或者,toggleIsMale()还可以这么写:
function toggleIsMale() {
setContact((prevStuInfo) => ({
...prevStuInfo,
isMale: !prevStuInfo.isMale,
}));
}

先将所有信息传入,再写要更改的属性。

Lazy State Initialization

有时候 state 的不断改变,会导致整个组件重复被加载,有些代码可能加载一次很费网络资源,而且也不需要加载,这时候需要设置 Lazy State Initialization(惰性状态初始化)

例如:

1
2
3
const [state, setState] = React.useState(() =>
console.log("Lazy State Initialization")
);

也就是设置 callback function。像上面这个例子,控制台输出一次该语句,就不用再重复输出了。

Sharing data between components 组件间共享数据

有时候子组件之间需要互传值怎么办?这时需要 raise state up to parent component.

在 React 里,数据只能从父组件下传到子组件,所以要想在子组件间共享某一个数值,则需要提前将该数据放在子组件共有且最近的父组件里。

Dynamic styles 动态样式

有时候一个组件或者元素的样式需要动态变化,那么需要做判断以决定用什么样式

1
2
3
4
5
6
7
const styles = {
backgroundColor: props.darkMode ? "#222222" : "#cccccc",
};

const squareElements = squares.map((square) => (
<div style={styles} className="box" key={square.id}></div>
));

这里要强调的是 style 里的属性不再是 CSS 里的那种,要写成驼峰式命名法 camel case

Conditional rendering 条件渲染

顾名思义,就是满足特定条件了,才能渲染指定内容

&& logical and operator 逻辑与运算符

When you want to either display something or NOT display it

以显示句子为例:

1
2
3
4
5
6
7
8
9
10
function toggleShown() {
setIsShown((prevShown) => !prevShown);
}
return (
<div>
{isShown && <p>{props.sentence}</p>}
<button onClick={toggleShown}>Show Sentence</button>
<hr />
</div>
);

ternary expression 三元表达式

When you need to decide which thing among 2 options to display

例子同上:

1
<button onClick={toggleShown}>{isShown ? "Hide" : "Show"} Sentence</button>

if else

When you need to decide between > 2 options on what to display

记住 return() 里的 {} 不能写 if else 语句,得在外边写

Form 表单

onChange

onChange:一个改变 value 就会触发的方法(表单常用)

以表单输入学生信息为例:

1
2
3
4
5
6
7
8
9
function handleChange() {
console.log("Changed!");
}

return (
<form>
<input type="text" placeholder="First Name" onChange={handleChange} />
</form>
);

event

表单元素里方法被触发后,可以通过接受 event 参数 来获得表单里的值

例子同上:

1
2
3
function handleChange(event) {
console.log(event.target.value);
}

event.target里面除了 value 之外,还有其它常用属性:

1
2
3
4
5
6
7
8
9
10
function handleChange(event) {
// 我们可以像下面这样接收
const { name, value, type, checked } = event.target;
setFormData((prevFormData) => {
return {
...prevFormData,
[name]: value,
};
});
}

state object

有时候表单元素很多,一个一个写 handleChange 比较麻烦,这时候需要 object 来统一存储表单里的值。

例子同上:

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
const [formData, setFormData] = React.useState({ firstName: "", lastName: "" });

function handleChange(event) {
const { name, value } = event.target;
setFormData((prevFormData) => {
return {
...prevFormData,
[name]: value,
};
});
}

return (
<form>
<input
type="text"
placeholder="First Name"
onChange={handleChange}
name="firstName"
value={formData.firstName}
/>
<input
type="text"
placeholder="Last Name"
onChange={handleChange}
name="lastName"
value={formData.lastName}
/>
</form>
);

Application: Checkbox

例子同上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function handleChange(event) {
const {name, value, type, checked} = event.target
setFormData(prevFormData => {
return {
...prevFormData,
[name]: type === "checkbox" ? checked : value
}
})
}

<input
type="checkbox"
id="isMale"
checked={formData.isMale}
onChange={handleChange}
name="isMale"
/>
<label htmlFor="isMale">Are you male?</label>

其中设置 id 是为了 htmlFor 能够定向到,即点击文字也可以选中按钮

Application: Radio buttons

例子同上:

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
function handleChange(event) {
const { name, value, type, checked } = event.target;
setFormData((prevFormData) => {
return {
...prevFormData,
[name]: value,
};
});
}

<fieldset>
<legend>Current employment status</legend>

<input
type="radio"
id="unemployed"
name="employment"
value="unemployed"
checked={formData.employment === "unemployed"}
onChange={handleChange}
/>
<label htmlFor="unemployed">Unemployed</label>

<input
type="radio"
id="employed"
name="employment"
value="employed"
checked={formData.employment === "employed"}
onChange={handleChange}
/>
<label htmlFor="employed">Employed</label>
</fieldset>;

Application: Select & Option

例子同上:

1
2
3
4
5
6
7
8
9
10
11
12
<label htmlFor="favColor">What is your favorite subject?</label>
<select
id="favSubject"
value={formData.favSubject}
onChange={handleChange}
name="favSubject"
>
<option value="">-- Choose --</option>
<option value="chinese">Chinese</option>
<option value="english">English</option>
<option value="math">Math</option>
</select>

Application: Submitting the form

1
2
3
4
5
6
7
8
9
10
11
function handleSubmit(event) {
event.preventDefault();
console.log(formData);
}

return (
<form onSubmit={handleSubmit}>
[html代码]
<button>Sumbit</button>
</form>
);
  1. <button>默认是type="submit",故这里可省略。
  2. event.preventDefault() 作用是阻止页面刷新,这样我们就可以正常运行handleSubmit方法。你可以理解成这是提交表单信息时必写的句子。

Side effects 副作用

useEffect()

有时候我们希望一些方法在组件渲染后运行,这时可以将代码写进 useEffect() 里,这样可以消除无限循环调用的副作用。比如请求数据等。

React.useEffect(func, [param1, parma2...])有两个参数:

func 是必填参数(方法),params 是可选参数(依赖,数组形式)

例如,当 count 的值变化了,则运行方法里的代码:

1
2
3
4
5
6
React.useEffect(
function () {
console.log("Effect ran");
},
[count]
);

Note

  1. 如果依赖里留空,则在组件第一次加载完毕后运行一次,之后不再运行。

    适合写 setInterval、请求数据等:

    1
    2
    3
    4
    5
    6
    useEffect(() => {
    const intervalId = setInterval(() => {
    setCount(prevCount => prevCount + 1)
    }, 1000)
    return () => clearInterval(intervalId)
    }, [])
  2. 在某些情况下,我们可能需要在组件卸载时清除副作用,这时可以在 useEffect 中 return 一个函数,这个函数会在组件卸载时执行,用于清除在 useEffect 中设置的副作用,比如取消请求或取消订阅事件等。上例就能说明。

  3. useEffect 可以在一个组件里写多个。

Using an async function inside useEffect (Clone)

useEffect takes a function as its parameter. If that function returns something, it needs to be a cleanup function. Otherwise, it should return nothing. If we make it an async function, it automatically retuns a promise instead of a function or nothing. Therefore, if you want to use async operations inside of useEffect, you need to define the function separately inside of the callback function, as seen below:

1
2
3
4
5
6
7
8
React.useEffect(() => {
async function getMemes() {
const res = await fetch("https://api.imgflip.com/get_memes");
const data = await res.json();
setAllMemes(data.data.memes);
}
getMemes();
}, []);

Component: Class

这是比较过时的概念,Bob Ziroll 希望我们也学习了解一下

index.js

1
2
3
4
5
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(<App type="Class" />, document.getElementById("root"));

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from "react";

// export default function App(props) {
// return (
// <h1>{props.type} component</h1>
// )
// }

export default class App extends React.Component {
render() {
return <h1>{this.props.type} component</h1>;
}
}

与 Function 组件 不同点是它多了一个this.,并且无需声明 props 参数 就可以直接通过this.props调用

State

看以下两种的区别

Function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from "react";

export default function App() {
const [goOut, setGoOut] = React.useState("Yes");

function toggleGoOut() {
setGoOut((prevState) => {
return prevState === "Yes" ? "No" : "Yes";
});
}

return (
<div className="state">
<h1 className="state--title">Should I go out tonight?</h1>
<div className="state--value" onClick={toggleGoOut}>
<h1>{goOut}</h1>
</div>
</div>
);
}

Class:

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
import React from "react";

export default class App extends React.Component {
/**
* A class component with state will ALWAYS save state in a class
* instance variable called `state`, which will always be an object.
* The individual values you save in state will be properties on
* the `state` object.
*
* The simplest (and more modern) way to delcare new state in a
* class component is to just use a "class field" declaring state
* as an object, like you see below.
*
* Then, throughout the rest of the component (e.g. inside the render
* method) you can access that state with `this.state.<yourPropertyHere>`
*/
state = {
goOut: "Yes",
};

/**
* Any class methods you create that need to call the `this.setState`
* method (which is available to our component because we're extending
* React.Component) should be declared as an arrow function, for
* reasons we will discuss soon. (Note: other class methods you
* want to make that DON'T use `this.setState` don't necessarily
* need to be declared as arrow function to work correctly)
*/
toggleGoOut = () => {
this.setState((prevState) => ({
goOut: prevState.goOut === "Yes" ? "No" : "Yes",
}));
};

render() {
return (
<div className="state">
<h1 className="state--title">Should I go out tonight?</h1>
<div className="state--value" onClick={this.toggleGoOut}>
<h1>{this.state.goOut}</h1>
</div>
</div>
);
}
}

Constructor method

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
import React from "react";

export default class App extends React.Component {
/**
* If you can't use class fields in your class components
* for some reason, then you'll need to make use of the
* class' `constructor` method to initialize your state object.
* The first line of the constructor method should be a call
* to `super()` like you see below, and then you can add your
* state variable as a property attached to `this`
*/
constructor(props) {
super(props);
this.state = {
goOut: "Yes",
};
this.toggleGoOut = this.toggleGoOut.bind(this);
}

/**
* If you can't use arrow functions for your class methods,
* you'll need to make sure to `bind` them inside the
* constructor above.
*/
toggleGoOut() {
this.setState((prevState) => {
return {
goOut: prevState.goOut === "Yes" ? "No" : "Yes",
};
});
}

render() {
return (
<div className="state">
<h1 className="state--title">Should I go out tonight?</h1>
<div className="state--value" onClick={this.toggleGoOut}>
<h1>{this.state.goOut}</h1>
</div>
</div>
);
}
}

Lifecycle methods: componentDidMount()

当组件挂载后再运行的 function

Lifecycle methods: componentDidUpdate()

当组件更新后再运行的 function

为了防止循环调用:

1
2
3
4
5
componentDidUpdate(prevProps, prevState) {
if(prevState.data !== this.state.data) {
this.getData(this.state.data)
}
}

当目前的数据和之前的不一样时,以此表明数据更新了,于是再来调用 function,否则会循环调用。

Lifecycle methods: componentWillUnmount()

在组件即将卸载前运行的 function

例如,在组建卸载时去除事件监听器(解绑事件):

1
2
3
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove)
}

Lifecycle methods: shouldComponentUpdate()

父组件更新会引起子组件的更新,有时子组件不需要更新,那么可以使用该钩子函数 shouldComponentUpdate(nextProps, nextState)

1
2
3
4
5
6
7
class Hello extends Component {
shouldComponentUpdate() {
//根据条件,决定是否重新渲染组件
return false
}
render() {...}
}

React 进阶

课程里最新 API:https://swapi.dev/api/people/1/

<React.Fragment>

它是一个虚拟容器,用来包含其它元素,而不会在渲染中对实际的 DOM 元素产生任何影响。

1
2
3
4
5
6
7
8
9
10
11
12
function MyComponent() {
return (
<React.Fragment>
<h1>My Title</h1>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</React.Fragment>
);
}

React 16.2 及更高版本中引入了简写语法,允许使用空标签 <></> 来表示 Fragment,而不是使用完整的语法 <React.Fragment></React.Fragment>

Props

Default Props

有时组件没有传入 props,这时需要写默认值

方法 1,定义<Conponent>.defaultProps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Card(props) {
const styles = {
backgroundColor: props.cardColor,
height: props.height,
width: props.width,
};

return <div style={styles}></div>;
}

Card.defaultProps = {
cardColor: "blue",
height: 100,
width: 100,
};

方法 2:

1
2
3
4
5
6
7
8
9
function Card(props) {
const styles = {
backgroundColor: props.cardColor || "black",
height: props.height || 100,
width: props.width || 100,
};

return <div style={styles}></div>;
}

Prop Types

有时 prop 值需要类型来限制/定义一下,防止组件传了错误类型的值。

1
2
3
4
5
6
import PropTypes from "prop-types"

Card.propTypes = {
cardColor: PropTypes.string.isRequired
height: PropTypes.number
}

Render Props

它是通过将渲染函数作为组件的 props 传递的方式。它允许一个组件将其 UI 渲染代码传递给另一个组件,以便在其上进行操作。

例如,假设有一个需要显示当前鼠标位置的组件,它可以使用 render props 将鼠标位置信息传递到 UI 上:

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
const MouseTracker = (props) => {
const [position, setPosition] = React.useState({ x: 0, y: 0 });

const handleMouseMove = (event) => {
setPosition({
x: event.clientX,
y: event.clientY,
});
};

return <div onMouseMove={handleMouseMove}>{props.render(position)}</div>;
};

const App = () => {
return (
<div>
<h1>Move the mouse around!</h1>
<MouseTracker
render={({ x, y }) => (
<p>
The mouse position is ({x}, {y})
</p>
)}
/>
</div>
);
};

Higher Order Components (HOCs) 高阶组件

可以让您重用组件的逻辑,并对组件进行自定义扩展。

比如给组件提供横向滚动的功能:

1
2
3
4
5
6
7
8
9
10
11
12
const withHorizontalScroll = (WrappedComponent) => {
return function (props) {
return (
<div style={{ overflowX: "scroll" }}>
<WrappedComponent {...props} />
</div>
);
};
};

// 用 HOC 包装组件,下方List是一个组件
export default withHorizontalScroll(List);

Performance Optimization

React.PureComponent

有时候父组件发生了状态或值的更改,其所有后代组件都会被迫渲染一次,很费时,将原来的Component换成React.PureComponent即可解决问题。

React.PureComponent只用于 Class 组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, {PureComponent} from "react"

class Child extends PureComponent {
render() {
console.log("[ ] [ ] [🧒🏻] [ ] rendered")
return (
<div>
<p>I'm a Child Component</p>
<GrandChild />
</div>
)
}
}

React.memo()

可用于 Function 组件,类似于高阶组件的用法。

1
2
3
4
5
6
7
8
9
10
11
function GrandParent() {    
console.log("[👴🏼] [ ] [ ] [ ] rendered")
return (
<div>
<p>I'm a GrandParent Component</p>
<Parent />
</div>
)
}

export default React.memo(GrandParent)

1
2
3
4
5
6
7
8
9
export default React.memo(function Parent() {
console.log("[ ] [👩] [ ] [ ] rendered")
return (
<div>
<p>I'm a Parent Component</p>
<Child />
</div>
)
})

Context

Context 提供了一种通过组件树传递数据的方法,而无需在每个级别手动传递 props。

Context Provider

Context Provider 相当于上下文的提供者,提供给组件使用共享的数据。它通常写在最低共同父组件的层级,这样既能方便相应组件访问 Context,也能准确控制 Context 数据被使用范围。

1
2
3
4
5
6
7
8
const ThemeContext = React.createContext()

ReactDOM.render(
<ThemeContext.Provider >
<App />
</ThemeContext.Provider>,
document.getElementById("root")
)

contextType

contextType 更像是组件接收 context 的容器,通过this.context来取值,但只有 Class 组件可以用。

下例是切换深浅模式功能:

Button.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import ThemeContext from "./themeContext"

class Button extends Component {
//写法1,仅下面1行
static contextType = ThemeContext
render() {
// console.log(this.context)
return (
<button className={this.context == 'light' ? 'light-theme' : 'dark-theme'}>Switch Theme</button>
)
}
}
//写法2
//Button.contextType = ThemeContext

index.js

1
2
3
4
5
6
7
8
import ThemeContext from "./themeContext"

ReactDOM.render(
<ThemeContext.Provider value={"dark"}>
<App />
</ThemeContext.Provider>,
document.getElementById("root")
)

Context Consumer

它更像是给 Function 组件使用的。

1
2
3
4
5
6
7
8
9
10
11
import ThemeContext from "./themeContext"

function Button(props) {
return (
<ThemeContext.Consumer>
{theme => (
<button className={`${theme}-theme`}>Switch Theme</button>
)}
</ThemeContext.Consumer>
)
}

Move Context Provider to its own component

把上下文提供者写成组件更好,代码细节应该封装到一个组件里,也方便写 State。

ThemeContext.js

1
2
3
4
5
6
7
8
9
10
11
12
import React from "react"
const {Provider, Consumer} = React.createContext()

const ThemeContextProvider = (props) => {
return (
<Provider value={"light"}>
{props.children}
</Provider>
)
}

export {ThemeContextProvider, Consumer as ThemeContextConsumer}

Note:value={"light"} 记得写上花括号 {},因为这只接收 object 形式

index.js

1
2
3
4
5
6
7
8
9
10
11
12
import React from "react"
import ReactDOM from "react-dom"

import App from "./App"
import {ThemeContextProvider} from "./themeContext"

ReactDOM.render(
<ThemeContextProvider>
<App />
</ThemeContextProvider>,
document.getElementById("root")
)

Button.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from "react"
import {ThemeContextConsumer} from "./themeContext"

function Button(props) {
return (
<ThemeContextConsumer>
{theme => (
<button className={`${theme}-theme`}>Switch Theme</button>
)}
</ThemeContextConsumer>
)
}

export default Button

Changing Context

获取上下文的值后,经常还需要更改它。

ThemeContext.js

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, {Component} from "react"
const {Provider, Consumer} = React.createContext()

class ThemeContextProvider extends Component {
state = {
theme: "dark"
}

toggleTheme = () => {
this.setState(prevState => {
return {
theme: prevState.theme === "light" ? "dark" : "light"
}
})
}

render() {
return (
<Provider value={{theme: this.state.theme, toggleTheme: this.toggleTheme}}>
{this.props.children}
</Provider>
)
}
}

export {ThemeContextProvider, Consumer as ThemeContextConsumer}

注意:<Provider>组件的 props 只能是 value,如果要传入多个变量,得写成 Object 形式。

Button.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from "react"
import {ThemeContextConsumer} from "./themeContext"

function Button(props) {
return (
<ThemeContextConsumer>
{context => (
<button onClick={context.toggleTheme} className={`${context.theme}-theme`}>Switch Theme</button>
)}
</ThemeContextConsumer>
)
}

export default Button

Hooks⭐️

React 内置的 Hooks 包括 useState, useEffect, useContext, useRef, useReducer(常用)

useMemo, useCallback, useImperativeHandle, useLayoutEffect, useDebugValue.

你也可以自定义 Hooks。

useRef

useRef的主要的功能就是帮助我们获取到DOM元素或者组件实例,它还可以保存在组件生命周期内不会变化的值。

有时候希望点了按钮后,聚焦仍在输入框里,这时可以用 useRef

下例是一个 Todo list,我只保留了与 useRef 相关代码,看看如何使用就好:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const inputRef = useRef(null)

function addTodo(event) {
event.preventDefault()
setTodosList(prevTodosList => [...prevTodosList, newTodoValue])
setNewTodoValue("")
inputRef.current.focus()
}

return (
<div>
<form>
<input ref={inputRef} type="text" name="todo" value={newTodoValue} onChange={handleChange}/>
<button onClick={addTodo}>Add todo item</button>
</form>
</div>
)

ref={inputRef}意思是 inputRef 变量指向该 input 输入框

inputRef.current.focus() 意思是聚焦于该变量指向的输入框(直接敲键盘可以输入,无需鼠标点击)

useReducer

If you wanna use useState() hook to manage a non-trivial(重要的)state like a list of items, where you need to add, update and remove items in the state. You end up facing a situation where state management logic takes a good part of the component body. To help you separate the concerns (rendering and state management) React provides the useReducer() hook. (跟 Redux 里的 Reducer 真的很像耶)

Syntax: const [state, dispatch] = useReducer(reducer, initialState);

initialState

In the case of a counter state, the initial value could be like:

1
2
3
const initialState = {
counter: 0
}

Note: the initialState can be a simple value but generally will contain an object.

reducer

The reducer function contains your custom state logic.

2 parameters: the current state and an action object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function reducer(state, action) {
let newState;
switch (action.type) {
case 'increase':
newState = {counter: state.counter + 1};
break;
case 'descrease':
newState = {counter: state.counter - 1};
break;
default:
throw new Error();
}
return newState;
}
action object

action object describes how to update the state. The property type describes what kind of state update the reducer must do. If the action object must carry some useful information (aka payload) to be used by the reducer, then you can add additional properties to the action object. Like user below.

1
2
3
4
5
6
7
const action = {
type: 'add',
user: {
name: 'Harris',
sex: 'male'
}
}

dispatch

The dispatch is a special function that dispatches an action object.

Syntax: dispatch(actionObject)

useContext

通过useContext拿到上下文里的数据。之前介绍过,这里直接上例子,看看和之前写的有什么不同(React进阶/Context/Move…),使用useContext代码会更简洁。

ThemeContext.js

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

const ThemeContext = React.createContext();

const ThemeContextProvider = (props) => {
const [theme, setTheme] = useState("light");

const toggleTheme = () => {
setTheme((prevTheme) => prevTheme === "light" ? "dark" : "light");
};

return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{props.children}
</ThemeContext.Provider>
);
};

export { ThemeContextProvider, ThemeContext };

index.js(保持不变)

1
2
3
4
5
6
7
8
9
10
11
12
import React from "react"
import ReactDOM from "react-dom"

import App from "./App"
import {ThemeContextProvider} from "./themeContext"

ReactDOM.render(
<ThemeContextProvider>
<App />
</ThemeContextProvider>,
document.getElementById("root")
)

Button.js

1
2
3
4
5
6
7
8
9
10
11
12
import React from "react"
import {ThemeContext} from "./themeContext"

function Button(props) {
//重点
const context = React.useContext(ThemeContext)
return (
<button onClick={context.toggleTheme} className={`${context.theme}-theme`}>Switch Theme</button>
)
}

export default Button

Custom Hooks

自定义 Hook 是一种让你能够将组件里可复用的逻辑提取到函数中的方式。自定义 Hook 可以让你在不增加组件的情况下,复用组件之间的状态逻辑,从而使代码更加简洁、清晰。

与 Component 直观区别是,Hook 是复用 JS 逻辑,Component 是复用 HTML 代码。

useCounter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import {useState} from "react"

function useCounter() {
const [count, setCount] = useState(0)

function increment() {
setCount(prevCount => prevCount + 1)
}

return [count, increment]
}

export default useCounter

Note:return [count, increment] 部分也可以写成 return {count, increment},这样的话,在接收时就必须使用该变量名了。

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, {useState} from "react"
import useCounter from "./useCounter"

function App() {
const [number, add] = useCounter()

return (
<div>
<h1>The count is {number}</h1>
<button onClick={add}>Add 1</button>
</div>
)
}

export default App

Router

React 路由 可以有效管理多个视图(组件)实现 SPA(single page application,单页应用)

Installation 安装

npm add react-router-dom

下面笔记是根据 V6 版本写的

<BrowserRouter> 与 <HashRouter>

在最外层包裹整个应用,它相当于 context

<BrowserRouter> :

1
2
3
4
5
6
7
8
9
10
import React from "react"
import ReactDOM from "react-dom"
import {BrowserRouter as Router} from 'react-router-dom'

ReactDOM.render(
<Router>
<App />
</Router>,
document.getElementById("root")
)

BrowserRouter模式,访问地址:https://<url>/xxx

<HashRouter> :

1
2
3
4
5
6
7
8
9
10
import React from "react"
import ReactDOM from "react-dom"
import {HashRouter as Router} from 'react-router-dom'

ReactDOM.render(
<Router>
<App />
</Router>,
document.getElementById("root")
)

HashRouter模式,访问地址:https://<url>/#/xxx

两者对比:

  • hash模式兼容好,history模式是 HTML5 提出的,兼容性差(移动端兼容性不错)
  • hash模式下,访问不存在的页面,不需要单独配置nginx。而history模式需要。
  • history模式路由更好看;hash模式带#号,不美观,而且处理描点链接不方便。

<Routes> & <Route>

<Routes> 它用来包裹 <Route>

<Route> 相当于 if 语句,配置路由规则和要展示的组件(路由出口),可以嵌套使用,需要通过<Outlet/> 组件来渲染子路由;

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
import { Routes, Route } from "react-router-dom";

<Routes>
{/* 1. path 定义路径,element 定义对应渲染组件 */}
<Route path="/" element={<Login />} />

<Route path="/layout" element={<WebLayout />}>
{/* 2. 嵌套路由需要<Outlet />组件配合使用,该路由访问路径http://localhost:8080/layout/main */}
<Route path="main" element={<Home />} />
</Route>

{/* 3. Route 也可以不写element属性,展示嵌套路由,对应路由是:/tab/list */}
<Route path="/tab">
<Route path="list" element={<List />} />
</Route>

{/* 4. Route 定义replace属性为true的时候,不会产生历史历史记录。默认是false */}
<Route path="/list" element={<List>} replace={true} />

{/* 5. Route 定义Index属性,不需要定义path属性,在子路由里显示该路由页面。 */}
<Route path="/main" element={<Main />}>
<Route index element={<Test1 />} />

{/* 6. 定义动态的路由,访问地址:http://localhost:8080/main/1 */}
<Route path=":id" element={<Test2 />} />
{/* 访问路径:http://localhost:8080/main/list/1 */}
<Route path="list/:id" element={<Test2 />} />
</Route>
</Routes>

<Link> 类似于a标签的,是基于a标签封装的组件。 最终<Link>组件会转为a标签。通常应用就是在不同页面之间跳转。

有三种传参方式:

  1. params
  2. searchParams
  3. state

必须被<BrowserRouter / > 或 <HashRouter / > 包裹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Link } from "react-router-dom";

{/* to的路由是对象,属性pathName。定义三种传参方式如下: */}
<Link to={{ pathName: "/" }}></Link>

{/* 1. params传参 👉 /detail/:id */}
<Link to="/detail/1"><Link>
<Link to={`/detail/${id}`}><Link>
<Link to="/detail/:id"><Link>

{/* 2. searchParams传参 👉 /detail?id=1 */}
<Link to="/detail?id=1"></Link>
<Link to=`/detail?id=${id}`></Link>

{/* 3. state 传参 */}
<Link to="/" state={{ id: 1 }}>

{/* replace 为true的时候,不产生历史记录, 默认replace={false} */}
< Link to="/detail" replace={ true }>

<NavLink> 组件和<Link> 组件使用一致,但它相当于激活版的 <Link>组件,可以实现导航被选中的高亮效果。通常的使用场景是在同一个界面,tab栏切换,导航高亮。

1
2
3
4
5
6
import { NavLink } from "react-router-dom";

// NavLink 组件的class 会返回含有isActive的对象,为true的时间,改导航路由被激活。
<NavLink to="/go" className={({ isActive }) => {
return isActive ? "red" : "blue"
}}> go </NavLink>

State 参数

state 是可选参数,通常在不同页面之间传递数据时,可以将数据隐式存储在 state 中,然后在目标页面中访问它们。它好处是不会将数据暴露在URL中,也不会被出现在浏览器历史记录里,从而保持URL的简洁性、美观性和保护用户隐私。

<Navigate>

<Navigate> 组件被渲染,路径被修改,切换视图。它和<Link>不同之处是,前者是渲染后跳转,后者需要点击事件发生才会跳转。

1
2
3
4
import { Navigate  } from "react-router-dom";

<Navigate to="/" />
<Navigate to="/" replace={true} />

<Outlet />

当 route 发生嵌套的时候,必须使用<Outlet />组件来渲染子路由对应的视图,相当于子路由渲染组件的占位符。

注意:它不能向即将要渲染的组件传值,它只用于显示组件。

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
import { Routes, Route, Outlet } from "react-router-dom";

function Module() {
return (
<div>
<h2>我是Modules组件</h2>
// 如果不写 <Outlet /> 组件,<Item /> 视图是渲染不出来的。
<Outlet />
</div>
)
}

function Item() {
return (
<div>
<h2>我是Item组件</h2>
</div>
)
}

function App() {
return (
<Routes>
<Route to="/tab" element={ <Module /> }>
<Route to="item" element={ <Item /> }>
</Route>
</Routes>
)
}

useRoutes() 🔥

接收数组生成路由表,动态创建 Routes 和 Route。类似 vue-router, 通过 js 配置路由。

Router.jsx

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
// 这里也可以定义纯js,定义router.js配置路由,类似vue中那样配置路由。
import React from "React"
import { useRoutes, Navigate } from "react-router-dom"

// 利用React.lazy 懒加载组件
const List = React.lazy(() => import("../views/list.jsx"))

// 懒加载组件
function lazyComponent(path) {
const Comp = React.lazy(() => import(`../views/${path}`));
return (
// fallcack 可自定义全局<Loading />组件,在切换页面或模块显示loading过程,提高用户体验。
<React.Suspense fallback={ <Loading title="加载中..." /> }>
<Comp />
</React.Suspense>
)
}

const routes = [
{
path: "/",
element: <Home />
},
{
path: "/list",
element: <List />
},
{
path: "/detail",
element: lazyComponent("detail.jsx")
},
{
path: "/login",
element: <Navigate to="/login" />
},
{
path: "/parent",
element: <Parent />,
children: [
{
index: true,
element: <Children />
},
{
path: "children1",
element: <Children1 />
children: [
{
path: "",
element: <Navigate to="/parent/children1/abc" />,
},
]
}
]
}
]

export const RouteList = () => {
const router = useRoutes(routes);
return <> { router } </>
}

App.js

1
2
3
4
import { RouteList } from "./Router.jsx"
function App(){
return <RouteList />
}

useNavigate()

它和 history.push() 类似,语法会更简洁些,React Router v6 版本建议使用它进行路由跳转。

通过 useNavigate()返回的参数 navigate 是一个函数。

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
import { useNavigate } from "react-router-dom"

export const Demo = () => {
const navigate = useNavigate();

const handle = () => {
navigate("/login", {
replace: true,
state: {
name: "error"
}
})
// 上述是state传参,params 和 searchParmas 传参的话, 需要手动拼接
// - params
navigate(`/detail/${id}`);
// - searchParmas
navigate(`/detail?name=${name}&age=${age}`);
}
return (
<div>
// 类似history.go(-1)
<button onClick={() => navigate(-1)}>后退</button>
// 类似history.go(1)
<button onClick={() => navigate(1)}>前进</button>

<button onClick={handle}>按钮</button>
</div>
)
}

useParams()

有时需要获取路由传来的值

App.js(定义路由)

1
2
3
4
<Routes>
<Route path="/services" element={<ServicesList/>} />
<Route path="/services/:serviceId" element={<ServiceDetail/>} />
</Routes>

ServicesList.js(链接传值)

1
2
3
4
5
6
7
8
9
10
11
12
13
function ServicesList() {
const services = servicesData.map(service => (
<h3 key={service._id}>
<Link to={`/services/${service._id}`}>{service.name}</Link>
</h3>
))
return (
<div>
<h1>Services List Page</h1>
{services}
</div>
)
}

ServiceDetail.js(用 useParams 获取链接值,serviceId这个变量名一定要与router里的名称对应)

1
2
3
4
5
6
7
8
9
10
11
import {useParams} from "react-router-dom"

function ServiceDetail(props) {
const {serviceId} = useParams()
const thisService = servicesData.find(service => service._id === serviceId)

return (
<h1>Service Detail Page</h1>
<div>{thisService}</div>
)
}

useSearchParams()

读取和修改当前位置的 URL 中的 query 对象

返回包含俩值的数组,内容分别为:当前的 search参数、更新search的函数

App.js

1
2
3
4
5
import { useSearchParams } from "react-router-dom"

<Routes>
<Route to="/list?name=harris&age=18" element={<Detail />} />
</Routes>

detail.jsx(访问路由https://<url>/list/1

1
2
3
4
5
6
7
8
9
10
11
import { useSearchParams } from "react-router-dom"

export const Detail = () => {
// 获取URL中携带过来的searchParams参数
const [search,setSearch] = useSearchParams();
const name = search.get("name"); // harris
const age = search.get("age"); // 18

// 点击,url变成http://localhost:3000/detail?name=zhang&age=19
return <div onClick={() => setSearch("name=Wendy&age=17")}>设置urlParams</div>
}

用了 set 方法后也只是生成了新的 url,要访问该它还得写一个页面跳转方法

useRouteMatch()

有时需要更改路由路径名称,那其全部子路由需要手动更改了,很麻烦,这时需要用 useRouteMatch,有了 useRoutes() 就不需要用这个了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import {Link, Routes, Route, useRouteMatch} from "react-router-dom"

function Profile() {
const {path, url} = useRouteMatch()

return (
<div>
<h1>Profile Page</h1>
<ul>
<li><Link to={`${url}/info`}>Profile Info</Link></li>
<li><Link to={`${url}/settings`}>Profile Settings</Link></li>
</ul>

<Routes>
<Route path={`${path}/info`}>
<Info/>
</Route>
<Route path={`${path}/settings`}>
<Settings/>
</Route>
</Routes>
</div>
)
}

Note: 官方文档建议 url 写在 <Link/> 里,path 写在 <Route/> 里。

useHistory()

有时要用 JS 代码实现页面跳转。但建议用 useNavigate()

使用 history.push("/pathname") 可跳转到路径为 /pathname 页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {useHistory} from "react-router-dom"

function ServiceDetail() {
const history = useHistory()

function handleClick() {
history.push("/services")
}

return (
<div>
<button onClick={handleClick}>Go back to all services</button>
</div>
)
}

例子里的 history 是 React 路由提供的,用于获取浏览器历史记录的相关信息,还有很多用法:

history.goBack():回退到上个页面

history.goForward():前进到下个页面

history.go(int):前进或回退到某个页面,参数 int 表示前进或后退页面数量(例如:-1 表示回退到上一页)

history.replace(/pathname): 替换当前页面历史记录,这样可以禁止用户回退到上一页面。

useLocation()

有时需要获取当前 URL 的位置信息,包括路径名 pathname、搜索参数 search、状态值 state 等等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { useLocation } from 'react-router-dom';

function MyComponent() {
const location = useLocation();
console.log(location) // 打印内容如下:
/*
{
hash: "",
key: "h8s9s",
pathname: "/list",
search: "?name=harris&age=18",
state: {a: 1, b: 2}
}
*/
}

如果在搜索框里输入 /list?name=harris&age=18

useOutlet()

在嵌套路由中使用,父组件使用 <Outlet />, 当子组件渲染的时候,会展示嵌套路由的对象。如果嵌套路由没有挂载,返回是null。

useOutlet()相较于<Outlet /> 的好处是:

  1. 可获取子路由相关信息
  2. 可定制化子路由的渲染逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { useOutlet } from 'react-router-dom';

function Dashboard() {
const outlet = useOutlet();

console.log(outlet.pathname); // "/dashboard"
console.log(outlet.parentParams); // 路由参数
console.log(outlet.parentPath); // "/dashboard"
console.log(outlet.route); // 当前路由对象
console.log(outlet.router); // 当前router对象
console.log(outlet.match); // 当前match对象

return (
<div>
{/* 渲染Outlet */}
{outlet}
</div>
);
}

useOutletContext()

Often parent routes manage state or other values you want to be shared with child routes. You can create your own context provider if you like, but this is such a common situation that it's built-into <Outlet />.

也就是用它可以向子路由组件传值。

Parent.jsx

1
2
3
4
function Parent() {
const [count, setCount] = React.useState(0);
return <Outlet context={[count, setCount]} />;
}

Child.jsx

1
2
3
4
5
6
7
import { useOutletContext } from "react-router-dom";

function Child() {
const [count, setCount] = useOutletContext();
const increment = () => setCount((c) => c + 1);
return <button onClick={increment}>{count}</button>;
}

Redirect 重定向

有时候需要页面重定向,比如检测是否登陆,若无就重定向到登陆界面。

语法:<Redirect to="/pathname" />

这是 React Router v4版本,v6 版本推荐用<Navigate />,关键还能传递参数和状态等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Navigate } from 'react-router-dom';

// 父亲路由
{
path: '/auth',
element: (
<Outlet />
),
children: [
{
path: '',
element: (
<Navigate to="/auth/login" />
)
},
{
path: '/login',
element: (
<Login />
)
}
]
}

Styled Components

Install

npm install --save styled-components

Advantages

  1. Automatic Critical CSS: Keep track of which components are rendered.
  2. No Class name bugs: Styled-components generate unique class names for your styles which means no duplicates or overlaps.
  3. Easy to find CSS: Styled-components make it super easy to find your CSS, as every bit of styling is tied to a specific component.
  4. Dynamic Styling: Adapting the styling of a component based on its props or a global theme.
  5. Automatic Vendor Prefixing: Write CSS the way you know and love, and let the package do it's magic.

Syntax

1
2
3
4
5
6
7
8
9
10
import styled from 'styled-components'

const Title = styled.h1`
color: #666;
`
const Content = () => {
return (
<><Title>💅 Section</Title></>
)
}

Styling through Props

1
2
3
4
5
6
7
8
9
10
import styled from 'styled-components'

const Title = styled.h1`
color: ${({color}) => color};
`
const Content = ({color}) => {
return (
<><Title color={color}>💅 Section</Title></>
)
}

color: ${({color}) => color} 中的 ({}) 最外层括号可有可无,中间的花括号是解构赋值语法,你用 props来获取值也可以。

比如:color: ${props => props.color}

JSON Server

Since we are focusing on the UI at the moment, we need to set up a dummy server(虚拟服务器), which will allow us to test our requests.

We can use json-server tool to create a full RESTful API from a JSON file.

JSON Server is a simple project that helps you to setup a REST API with CRUD operations very fast.

Creating the db.json

To be able to use the json-server tool, first we need to create a db.json file, which is going to contain our full database for the server.

The json-server tool will allow you to make the following:

  • GET requests, to fetch data from the file.

  • POST requests, to insert new data into the file.

  • PUT and PATCH requests, to adjust existing data.

  • DELETE requests, to remove data.

Let's create src/server/db.json.

1
2
3
4
5
6
7
8
9
{
"posts": [
{"id": 1, "title": "React", "content": "It's a good tool."},
{"id": 2, "title": "Looking for a job", "content": "It's hard to find a job."}
],
"user": [
{"id": 1, "username": "Harris Wong", "pwd": "123456"}
]
}

We need to make sure that we provide an id value so that we can query the database later.

Installing the json-server tool

  1. We're gon' install the json-server tool via npm:

    npm install --save json-server

  2. Now, we can start our backend server by calling the following command:

    npx json-server --watch server/db.json

The --watch flag means that it'll listen to changes to the files, and refresh automatically.

Output

Now, we can go to http://localhost:3000/posts/1 to see our post object.

The result will be like this:

1
2
3
4
5
{
"id": 1,
"title": "React",
"content": "It's a good tool."
}

Configuring the package.json

We need to change the port, so that it doesn't run on the same port as our client.

We need to install a tool called concurrently, which lets us start the server and client at the same time:

npm install --save concurrently

package.json

1
2
3
4
5
"scripts":{
"start": "npx concurrently \"npm run start::server\" \"npm run start:client\"",
"start:server": "npx json-server --watch server/db.json --port 4000",
"start:client": "react-scripts start",
}

Now, running npm start will run the client, as well as the backend server.

Configuring a proxy

Finally, we have to define a proxy(代理), to make sure that we can request our API from the same Uniform Resource Locator (URL) as the client.

This is needed because, otherwise, we would have to deal with cross-site requests, which are a bit more complicated to handle.

We are going to define a proxy that will forward requests from http://localhost:3000/api/to http://localhost:4000/.

Now, let's configure the proxy:

  1. npm install --save http-proxy-middleware

  2. Then, we create a new src/setupProxy.js file:

    1
    2
    3
    4
    5
    6
    7
    8
    const proxy = require('http-proxy-middleware')

    module.exports = function(app) {
    app.use(proxy('/api', {
    target: 'http://localhost:4000',
    pathRewrite: {'^/api': ''}
    }))
    }

    target: the target of our proxy is the backend server.

    pathRewrite: It'll remove the /api prefix before forwarding the request to our server.

The preceding proxy configuration will link /api to our backend server, therefore, we can now start both the server and the client via npm start.

Then, we can access the API by opening http://localhost:3000/posts/1.

技巧合集

快捷代码指令插件

在 VSCode 里安装ES7+ React/Redux/React-Native snippets插件后,可以通过键入rafce快捷创建一个 function 组件

项目

目录结构

通常,src 目录结构如下:

1
2
3
4
5
6
7
8
9
src/					项目源码,写项目功能代码
assets/ 资源(图片、字体等)
components/ 公共组件
pages/ 页面
containers/ 容器
utils/ 工具
App.js 根组件(配置路由信息)
index.css 全局样式
index.js 项目入口文件(渲染根组件、导入组件库)

components/里放一些公用 UI 组件,包括导航栏和页尾等

containers/里放需要与数据和状态有关的 UI 组件,负责从数据层获取数据,处理用户交互,然后传递数据到 UI 组件进行呈现。

Project Setup 项目安装 1

这里使用的是 Vite,它是一种新型前端构建工具,能够显著提升前端开发体验

1
2
3
4
npm create vite@latest
y
react
javascript

或者 👇

Project Setup 项目安装 2

初始化项目npx create-react-app <project-name>

启动项目

  • Development mode: npm run dev
  • Production mode: npm start

部署项目

将 React 项目部署到 GitHub 和 Vercel 需要以下步骤:

部署到 GitHub

  1. 在GitHub上创建一个新的repository,并将你的代码推送到这个repository中。

  2. 确保你的代码已经经过了所有的测试,并且已经成功运行过。

  3. 在你的React项目中安装GitHub Pages插件:

    npm install gh-pages --save-dev

  4. 打开package.json文件,添加以下代码到文件中:

    1
    2
    3
    4
    5
    "homepage": "https://<your-username>.github.io/<your-repository-name>",
    "scripts": {
    "predeploy": "npm run build",
    "deploy": "gh-pages -d build"
    }

    注意:homepage中的URL应该与你的GitHub repository相对应。

  5. 运行以下命令将你的应用程序部署到 GitHub Pages:

    1
    2
    npm run build
    npm run deploy

部署到 Vercel

  1. 首先,你需要创建一个 Vercel 账户,并登录到 Vercel 控制台。
  2. 在控制台中,点击“New Project”,并选择“Import Git Repository”选项,将你的GitHub repository导入到Vercel中。
  3. 确保你的项目中有一个package.json文件,并在其中设置start脚本。例如:
1
2
3
"scripts": {
"start": "react-scripts start"
}
  1. 点击“Deploy”,等待Vercel构建和部署你的应用程序。
  2. 一旦部署成功,你的应用程序将会自动在Vercel上运行。

实用工具库

Ant Design

安装

npm install antd --save

自定义主题 & 深浅切换

自定义主题在官网可以自己定制主色、尺寸等等,然后复制代码到 token 这里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { theme, ConfigProvider, Button } from "antd";
import { useState } from "react";
import "./App.css";

function App() {
const [darkMode, setDarkMode] = useState(false);

return (
<ConfigProvider
theme={{
token: {
colorPrimary: "#ff8a00",
},
algorithm: darkMode ? theme.darkAlgorithm : theme.defaultAlgorithm
}}
>
<div className="App">
<Button type="primary" onClick={() => setDarkMode(!darkMode)}>Button</Button>
</div>
</ConfigProvider>
);
}

export default App;

MUI

安装

npm install @mui/material @emotion/react @emotion/styled

自定义主题 & 深浅切换

可以用 mui-theme-creator 帮助设计和自定义 MUI 组件库主题,还可以用 Material palette generator 为您输入的任何颜色生成调色板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { ThemeProvider, createTheme } from "@mui/material";

function App() {
const theme = createTheme({
palette: {
mode: 'light',
primary: {
main: '#3d993d',
},
},
});

return (
<>
<ThemeProvider theme={theme}>
<Router />
</ThemeProvider>
</>
);
}

自定义主题组件

您可以使用主题中的键来自定义组件的样式、默认props等。这有助于在整个应用程序中实现样式的一致性。

echarts-for-react

这是一个简单的 Apache echarts 的 React 封装。

npm install --save echarts-for-react

react-icons

npm install react-icons

参考文章