在React中,V16版本之前有三种方式创建组件( 被删除了),之后只有两种方式创建组件。这两种方式的组件创建方式效果基本相同,但还是有一些区别,这两种方法在体如下:createClass()
本节先了解下用Function函数的方式创建React组件,有了上几节的铺垫,所以本节的代码示例部分无用的代码会被过滤掉以节省篇幅。
通过Function函数方式创建组件(推荐)
代码放在一个单独的.js文件中
//HelloReact.js, 组件文件的名称最好也要大写,以方便与非组件的js区分开
function MyComponent() { //名称要大写
return (
<img
src="https://i.imgur.com/MK3eW3As.jpg"
alt="Katherine Johnson"
/>
);
}
export default function Gallery() { //发布组件,可以这样来写
return (
<section>
<h1>了不起的科学家</h1>
<MyComponent />
<MyComponent />
</section>
);
}
调用
import './App.css';
import Gallery from "./fun/helloReact";
function Fun() {
return (
<div className="fun">
<h3>HelloReact</h3>
<Gallery/>
</div>
);
}
export default Fun;
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
<Fun/>
</React.StrictMode>
);
- 组件名称,必须以大写字母开头;
- return方法需要注意:如果只一行代码,则可以不写
()
,而且必须和return在同一行。如果有多行代码必须写()
,否则没有括号包裹的话,任何在 return 下一行的代码都 将被忽略! - 不要在组件中嵌套组件定义,因为会严重影响性能;
核心属性
props
函数式组件定义参数会有两种写法,用哪一种都可以:
语法格式
- 第一种:明确定义多个参数,
{ }
//这种方式,一定要注意参数外面的的 {}
function Avatar({ person, size }) {
// ...
}
- 第二种
function Avatar(props) {
let person = props.person;
let size = props.size;
// ...
}
- 以上两种参数定义的方式的调用方式相同
<Avatar
person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }}
size={100}
/>
针对以上两种传值时还可以有如下扩展
设置默认值
export function Avatar1({ person, size=100 }) {
return (
<div>{JSON.stringify(person)}, {size}</div>
);
}
export default function Form({status = 'empty'}) {
if (status === 'success') {
return <h1>That's right!</h1>
}
return (
<>
<h2>City quiz</h2>
</>
)
}
传递子元素
注意使用内置的children
属性
export function Avatar3(props) {
return (
<div>{props.children}</div>
);
}
//调用方式
<Avatar3>
<p>我是嵌套组件</p>
</Avatar3>
唯一需要注意的是,props是不可变的,如果是可变的参数要放置在state中。
ref
使用 ref 操作Html原生DOM
由于 React 会自动处理更新 DOM 以匹配你的渲染输出,因此你在组件中通常不需要操作 DOM。但是,有时你可能需要访问由 React 管理的 DOM 元素 —— 例如,让一个节点获得焦点、滚动到它或测量它的尺寸和位置。在 React 中没有内置的方法来做这些事情,所以你需要一个指向 DOM 节点的 ref 来实现。
ref
和ref.current
import { useRef } from 'react';
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>
聚焦输入框
</button>
</>
);
}
useRef()返回一个对象,该对象有一个名为 current 的属性。最初,myRef.current 是 null。当 React 为这个<div>
创建一个 DOM 节点时,React 会把对该节点的引用放入 myRef.current。然后,你可以从 事件处理器 访问此 DOM 节点,并使用在其上定义的内置浏览器 API。
使用 ref 操作自定义组件DOM
将 ref 放在你自己的组件上,默认情况下你会得到 null,而且会得到一个错误
Cannot read properties of null (reading 'focus')
。比如下面的代码:
import { useRef } from 'react';
function MyInput(props) {
return <input {...props} />;
}
export default function MyForm() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
聚焦输入框
</button>
</>
);
}
但可以用另一种机制forwardRef
解决,MyInput 组件是使用 forwardRef 声明的。 这让从上面接收的 inputRef 作为第二个参数 ref 传入组件,第一个参数是 props 。MyInput 组件将自己接收到的 ref 传递给它内部的 <input>
就起作用了。
import { forwardRef, useRef } from 'react';
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
聚焦输入框
</button>
</>
);
}
事件交互
绑定事件
普通的事件绑定,代码一般写在最上面。这里的handleClick也可以做为一个参数,由父组件传入。
export default function Button() {
function handleClick() {
alert('你点击了我!');
}
return (
<button onClick={handleClick}>
点我
</button>
);
}
其它的简单写法,如果不是由父组件传入时,不是太推荐这种写法。因为只有当函数体较短时,内联事件处理函数会很方便。
<button onClick={function handleClick() {
alert('你点击了我!');
}}>
<button onClick={() => {
alert('你点击了我!');
}}>
需要注意
{handleClick}
后面没有()
,如果写了括号,在加载组件时就会在渲染时执行而在点击时不会执行。
事件传参
主要用了这个特性,<button onClick={(e)=>handleClick1(e,'tt')}>
export default function Button() {
function handleClick1(e, args ) {
console.log(args);
console.log(e.target);
}
return (
<div>
<button onClick={handleClick}>
点我
</button>
<button onClick={(e)=>handleClick1(e,'tt')}>
点我带参数
</button>
</div>
);
}
发送请求
采用内置的post函数即可
post('/analytics/event', { eventName: 'visit_form' });
将事件处理函数作为 props 传递
就是把点击事件定义在组件外面,这样复用度会更高,下面是一个简单的工具栏的例子:
function Button({ onClick, children }) {
return (
<button onClick={onClick}>
{children}
</button>
);
/* 注意这种替代写法
<button onClick={e => {
e.stopPropagation();
onClick();
}}>
*/
}
function PlayButton({ movieName }) {
function handlePlayClick() {
alert(`正在播放 ${movieName}!`);
}
return (
<Button onClick={handlePlayClick}>
播放 "{movieName}"
</Button>
);
}
export default function Toolbar() {
return (
<div>
<PlayButton movieName="魔女宅急便" />
</div>
);
}
事件传播和默认行为
这是JS的一个特性,即事件会沿着DOM树向上“冒泡”或“传播”:它从事件发生的地方开始,然后沿着树向上传播。这种机制有好处有坏处,好处是我们可以根据这种特性自动调用外层组件的相同方法。,另一个就是事件捕获,捕获事件对于路由或数据分析之类的代码很有用。
function handleClick(e) {
e.stopPropagation(); //防止事件传播
alert('你点击了我!');
}
另一个相关的就是阻止默认行为,比如表单的onSubmit事件,
e.preventDefault()
;
组件间通信
父子间数据传递
对于父子组件就不细说了,可以查看前一篇文章:
- 父到子:通过props传递参数;
- 子到父:通过回调函数更改state通信
跨级组件间通信
通常来说,你会通过 props 将信息从父组件传递到子组件。但是,如果你必须通过许多中间组件向下传递 props,或是在你应用中的许多组件需要相同的信息,传递 props 会变的十分冗长和不便。Context 允许父组件向其下层无论多深的任何组件提供信息,而无需通过 props 显式传递。
Context 让父组件可以为它下面的整个组件树提供数据,大概原理如下:
- 声明 – LevelContext.js’
import { createContext } from 'react';
export const LevelContext = createContext(1); //LevelContext就是参数名,后面1也可以换成一个复杂的对象
- 使用 – Heading.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
const level = useContext(LevelContext);
- 提供-- Section.js
上面其实就完成了一个使用过程,相当于一个全局变量,但还没有传值这么一块。上面代码leavl=1,加了Section.js后level=level的值。
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}> {/*参数*/}
{children}
</LevelContext.Provider>
</section>
);
}
//调用
<Section level={2}>
这告诉 React:“如果在<Section>
组件中的任何子组件请求 LevelContext参数,给他们这个 level(2)”。组件会使用 UI 树中在它上层最近的那个 <LevelContext.Provider> 传递过来的值。
State
React.useState
state的作用不用多说了,使用方式如下:
import { useState } from 'react';
export default function GalleryWall() {
//index 是一个 state 变量,setIndex 是对应的 setter 函数, 后面的0表示index变量的默认值,定义成const的原因也是为了限制只能通过setIndex()方法来修改,不要直接用==来赋值。
const [index, setIndex] = useState(0);
function handleClick() {
//调用 setIndex 方法相当于改变了state中的数据,然后会自动调用render()函数渲染页面。
setIndex(index + 1);
}
return (
<div>{index}</div>
);
在 React 中,useState 以及任何其他以“use”开头的函数都被称为 Hook,作用是可以让组件中使用不同的 React 特性,本质是个函数,但不建议在条件语句、循环语句或其他嵌套函数内调用 Hook。因为这样会比较影响性能。
多个state的写法同上类似
export default function Gallery() {
const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);
}
使用state重绘UI
这个例子主要是利用了state值变化会自动重新渲染组件的能力,点击按钮后会替换为一个div。
import {useState} from "react";
export default function Repaint() {
const [isRepainting, setIsRepainting] = useState(false);
if (isRepainting){
return(
<div>重绘了组件UI</div>
)
}
function handleClick(e) {
setIsRepainting(true);
}
return (
<div>
<button onClick={handleClick}>
点我重绘
</button>
</div>
);
}
state的批处理
这主要是处理特殊情况,原因是state在一次渲染过程中间其值不会变化的。比如下面两行代码。
const [number, setNumber] = useState(0);
//最终值为1,因为在一次渲染过程中虽然调用了三次,但其number值始终为0
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
//最终值为3,下面是一个固定写法,React的一个队列功能,其中n为自定义的一个变量名
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
更新 state 中的对象
state 中可以保存任意类型的 JavaScript 值,包括对象。但是,你不应该直接修改存放在 React state 中的对象。相反,当你想要更新一个对象时,你需要创建一个新的对象(或者将其拷贝一份),然后将 state 更新为此对象。应该将 state 视为只读的。
const [position, setPosition] = useState({ x: 0, y: 0 });
:表示position的初始值为{ x: 0, y: 0 }。
不能直接改变postion的值,应该像下面这样
setPosition({
x: e.clientX,
y: e.clientY
});
或是使用如下语法复制一个对象(下面有简写方式):
const nextPosition = {};
nextPosition.x = e.clientX;
nextPosition.y = e.clientY;
setPosition(nextPosition);
如果是表单时,可采用下面的方法,即统一参数名,然后用event.target.name
这种方式来更新
import { useState } from 'react';
export default function Form() {
const [person, setPerson] = useState({
firstName: 'Barbara',
lastName: 'Hepworth'
});
function handleChange(e) {
setPerson({
...person,
[e.target.name]: e.target.value
});
}
return (
<>
<label>
First name:
<input
name="firstName"
value={person.firstName}
onChange={handleChange}
/>
</label> {/**省略lastName/}
<p>
{person.firstName}{' '}
</p>
</>
);
}
更新state中的数组
需要注意两个方法:
- slice 让你可以拷贝数组或是数组的一部分。
- splice 会直接修改 原始数组(插入或者删除元素)(不能用)
以下是一个添加的例子,注意代码的执行流程:
const [name, setName] = useState('');
const [artists, setArtists] = useState([]);
let nextId = 0;
return (
<>
<h1>振奋人心的雕塑家们:</h1>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<button onClick={() => {
setArtists([
...artists,
{ id: nextId++, name: name }
]);
}}>添加</button>
<ul>
{artists.map(artist => (
<li key={artist.id}>{artist.name}</li>
))}
</ul>
</>
);
使用展开…语法复制对象
上面的例子可用下面代码来书写,如果没有后面的x: e.clientX就是一个复制。
setPosition({
...position, // 复制上一个 position 中的所有字段
x: e.clientX // 但是覆盖 x 字段
});
但需要注意 … 展开语法本质是是“浅拷贝”——它只会复制一层。这使得它的执行速度很快,但是也意味着当你想要更新一个嵌套属性时,你必须得多次使用展开语法。
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson);
或
//推荐
setPerson({
...person, // 复制其它字段的数据
artwork: { // 替换 artwork 字段
...person.artwork, // 复制之前 person.artwork 中的数据
city: 'New Delhi' // 但是将 city 的值替换为 New Delhi!
}
});
使用 Immer 插件语法复制对象
npm install use-immer
用 import { useImmer } from 'use-immer' 替换掉 import { useState } from 'react'
import { useImmer } from 'use-immer';
export default function Form() {
const [person, updatePerson] = useImmer({
name: 'Niki de Saint Phalle',
artwork: {
city: 'Hamburg',
}
});
function handleNameChange(e) {
updatePerson(draft => {
draft.name = e.target.value;
});
}
function handleCityChange(e) {
updatePerson(draft => {
draft.artwork.city = e.target.value;
});
}
return (
<>
<label>
Name:
<input value={person.name} onChange={handleNameChange} />
</label>
<label>
City:
<input
value={person.artwork.city}
onChange={handleCityChange}
/>
</label>
<p>
{person.name} (located in {person.artwork.city})
</p>
</>
);
}
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
const [myList, updateMyList] = useImmer(
initialList
);
//id=序号,nextSeen=true|false,即单值框的值,
function handleToggleMyList(id, nextSeen) {
updateMyList(draft => {
const artwork = draft.find(a =>
a.id === id
);
artwork.seen = nextSeen;
});
}
//ItemList是一个另外封装的组件
return (
<>
<h1>艺术愿望清单</h1>
<h2>我想看的艺术清单:</h2>
<ItemList
artworks={myList}
onToggle={handleToggleMyList} />
</>
);
对 state 进行保留和重置
相同位置的相同组件会使得 state 被保留下来。这是React的一个特性,这就造成了一个问题,比如像书本这样的应用,翻页时会显示在同样的位置,如果在前一页做了操作,如果state会保留那么这个状态会带到下一页上去,就会乱套了。
对 React 来说重要的是组件在 UI 树中的位置,而不是在 JSX 中的位置!
保留
import { useState } from 'react';
export default function App1() {
const [isFancy, setIsFancy] = useState(false);
return (
<div>
{isFancy ? (
<Counter isFancy={true} />
) : (
<Counter isFancy={false} />
)}
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
使用好看的样式
</label>
</div>
);
}
function Counter({ isFancy }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = {color:"red"};
if (isFancy) {
className = {color:"yellow"};;
}
return (
<div
style={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
加一
</button>
</div>
);
}
当切换颜色时,上面的数字会保留之前的样式,这是因为 相同位置的相同组件会使得 state 被保留下来 。因为React认为是同一个组件。
key使用重置
如果把上面的代码,改成如下代码,则每次切换颜色都会归0,因为用key了表示是不同的组件,不能保留的原因是重新绘制时把组件给删除了,此时状态也消失了。如果需要保留的话把它隐藏即可。或是把状态提升到父类中,在重新绘制时再以参数的方式传递进来。
{isFancy ? (
<Counter key="blue" isFancy={true} />
) : (
<Counter key="yellow" isFancy={false} />
)}
用useReducer替换useState集中状态变化函数
对于拥有许多状态更新逻辑的组件来说,过于分散的事件处理程序可能会令人不知所措。对于这种情况,你可以将组件的所有状态更新逻辑整合到一个外部函数中,这个函数叫作 reducer。
//替换对象
import { useReducer } from 'react';
/*
* action:action自定义的对象
* currState:当前状态
* */
function tasksReducer(currState, action) {
switch (action.type) {
case 'added': {
console.log(action.id);
return currState+1;
}
case 'deleted': {
return currState-1;
}
default: {
throw Error('未知 action:' + action.type);
}
}
}
export default function UseState() {
//定义状态, tasksReducer为自定义的函数
const [score, dispatch] = useReducer(tasksReducer, 0);
function added(){
// "action" 对象:它是一个普通的 JavaScript 对象。它的结构是由你决定的
dispatch({
type: 'added',
id:11 //多余的参数,直接封装在action中
})
}
function deleted(){
dispatch({
type: 'deleted',
})
}
return (
<div>
<input type="text" value={score}/>
<button onClick={added} >+1</button>
<button onClick={deleted} >-1</button>
</div>
);
}
因为 Reducer 和 Context 就是操作state的,所以可以结合这两个元素在一起,提供一个更复杂的状态管理,比如下面这样的示例代码:
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
严格模式
严格模式主要用于开发环境帮助找到错误,在生产环境下不生效。在严格模式下开发时,它将会调用每个组件函数两次。通过重复调用组件函数,严格模式有助于找到违反一些规则的代码,比如不纯粹的组件等。
import React from 'react';
root.render(
<React.StrictMode> {/* 严格模式 */}
<App />
<Fun/>
</React.StrictMode>
);
组件导出和导入
可以直接写成下面这样的代码,因为我们的宗旨是构建小而美的组件,所以下列的代码完全合理:
//helloReact.js
export default function Gallery(){ }
export function MyComponent(){ }
语法 | 导出语句 | 导入语句 |
---|---|---|
默认 | export default function Button() {} | import Button from ‘./Button.js’; 这里的Button名字可以自定义 |
具名 | export function Button() {} | import { Button } from ‘./Button.js’; |
- 最后的.js可以省略
- 同一文件中,有且仅有一个默认导出,但可以有多个具名导出!
- 默认导出时,在导入时的名字可以自定义,但具名导入时必须和导出的名字一样,但具名导出一般多用于工具类的时候会比较多,比如下面这个工具类:
export function getImageUrl(person, size = 's') {
return (
'https://i.imgur.com/' +
person.imageId +
size +
'.jpg'
);
}
特殊用法
以下这些用法有时会用不到,但还是需要了解下
纯数据组件
//quotes.js
export default [
"Don’t let yesterday take up too much of today.” — Will Rogers",
"Ambition is putting a ladder against the sky.",
"A joy that's shared is a joy made double.",
];
//InspirationGenerator.js
import quotes from './quotes';
export default function InspirationGenerator({children}) {
const quote = quotes[index];
return (
<>
<FancyText text={quote} />
</>
);
}