组件
创建组件的方式
创建组件有函数组件和类组件两种方式,React18 之后,全面使用函数组件,类组件会退出历史舞台
function Home(props) {
return <div className="home">Welcome to React~</div>;
}
const Home = () => {
return <div className="home">Welcome to React~</div>;
};
对组件的要求
- 组件名称必须以大写字母开头,否则 React 会将以小写字母开头的组件视为原生 DOM 标签
- 必须返回可以渲染的元素
- react 元素
- null
- 组件
- 可迭代的对象,包括数组、Set、Map 等
function App1() {
return null;
}
function App2() {
return [1, 2, 3];
}
// 如果直接返回对象,会报错:Uncaught Error: Objects are not valid as a React child
function App3() {
return { a: 1 };
}
那么是否可以说 React 组件不能返回对象?不能,可以返回一个迭代器
const obj = { a: 1 };
obj[Symbol.iterator] = function* () {
for (let prop in obj) {
yield [prop, obj[prop]];
}
};
function App() {
return obj;
}
组件重新渲染的条件
- 自身状态发生变化
- 父组件重新渲染
数据
所有 React 组件都必须像纯函数一样保护它们的props
不被更改
改变数据核心思想:先拷贝这个对象或数组,再改变这个拷贝后的值
更新对象:创建一个新的对象,通常使用展开运算符
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg'
}
});
如果要更新 person.artwork.city 的值
setPerson(...person, artwork: { ...person.artwork, city: 'beijing'})
更新数组:
添加 setList([...list, 666])
删除,需要生成一个不包含该元素的新数组,通常使用 filter 方法
更新,map()
Fragments
简单说就是避免向 DOM 中添加额外的节点
假如有一个子组件<Columns />
class Columns extends React.Component {
render() {
return (
<div>
<td>Hello</td>
<td>World</td>
</div>
);
}
}
有一个父组件使用了<Columns />
class Table extends React.Component {
render() {
return (
<table>
<tr>
<Columns />
</tr>
</table>
);
}
}
结果如下,在 tr 和 td 之间多了一个 div 节点,这样就导致了 html 是无效的
<table>
<tr>
<div>
<td>Hello</td>
<td>World</td>
</div>
</tr>
</table>
Fragments
就解决了这个问题
class Columns extends React.Component {
render() {
return (
<React.Fragment>
<td>Hello</td>
<td>World</td>
</React.Fragment>
);
}
}
也可以使用一种短语法,像空标签一样
class Columns extends React.Component {
render() {
return (
<>
<td>Hello</td>
<td>World</td>
</>
);
}
}
严格模式
在脚手架生成的 main.js 中,会发现使用了严格模式 StrictMode。启用了严格模式后, React 会在开发环境下调用渲染函数两次
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
作用:
- 检查组件是否是纯函数
- 及早发现 useEffect 中的错误
- 警告过时的 API
受控组件和非受控组件
在对表单进行处理时,需要考虑:
- 非受控组件:由用户控制 value
- 受控组件:由代码控制 value
非受控组件:代码可以设置表单的初始值 defaultValue,能改变 value 的只有用户,代码通过 onChange 事件监听用户输入。
import { ChangeEvent, useState } from 'react';
function App() {
console.log('render');
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
return <input type="text" defaultValue={'hello'} onChange={handleChange} />;
}
export default App;
受控组件:由代码改变 value 的值。如下示例,input 的值是通过inputValue
控制的,在 onChange 事件中通过setInputValue
更新inputValue
的值,这样就实现了受控组件。如果将setInputValue(e.target.value)
注释掉,在输入时,会发现控制台打印的是最新的值,但是页面上 input 是不能输入的。
import { ChangeEvent, useState } from 'react';
function App() {
const [inputValue, setInputValue] = useState('hello');
console.log('render');
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
setInputValue(e.target.value);
};
return <input type="text" value={inputValue} onChange={handleChange} />;
}
export default App;
React 表单内置的受控组件的行为:
- value + onChange,如
<input type="text" value={inputValue} onChange={handleChange} />
- checked + onChange,如
<input type="checkbox" checked={checked} onChange={handleCheckedChange} />
受控组件的使用场景
运行前面的示例时,可以发现,非受控组件在初始打印一次 render,在输入时则不会打印,但是受控组件在输入时也会打印,说明受控组件会引起组件重新渲染。
受控组件的使用场景:
- 要处理输入的值
- 实时同步状态值到父组件
比如,在输入时,将输入的值转换为大写,如下示例:
import { ChangeEvent, useState } from 'react';
function App() {
const [inputValue, setInputValue] = useState('hello');
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value.toUpperCase());
};
return <input type="text" value={inputValue} onChange={handleChange} />;
}
export default App;
ant design 这些组件库都是支持受控和非受控的方式,如果要使用受控组件,则使用value
属性,如果要使用非受控组件,则使用defaultValue
属性。在组件内部通过判断 value 的值是否是 undefined 来判断是否受控。
组件嵌套
如果想在某个组件里嵌套子组件,需要在该组件里通过props.children
接收子组件的内容,否则不显示子组件的内容
App 组件:
import Child from './Child';
import Foo from './Foo';
function App() {
return (
<>
<Child>
<Foo />
</Child>
</>
);
}
export default App;
Foo 组件:
function Foo() {
return <div>Foo</div>;
}
export default Foo;
Child 组件:
function Child({ children }) {
return (
<>
<div>Child</div>
{children}
</>
);
}
export default Child;
Q:为什么不直接在 Child 组件里使用 Foo 组件?
A:因为有时需要从上层组件(这里是 App 组件)获取数据,而不是从 Child 组件获取
Lazy 与 Suspense 实现懒加载
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>
示例 1:在 App.jsx 中引入 Foo 组件,但是并未使用,但是控制台会打印出 Foo。这个时候就没有做到按需加载
import Foo from './Foo';
function App() {
return (
<>
<div>App</div>
</>
);
}
export default App;
console.log('Foo');
function Foo() {
return <div>Foo</div>;
}
export default Foo;
再看一个项目中经常遇到的代码如下,希望在 show 变为 true 时,再加载 Foo 组件,实际上控制台依然打印出了 Foo
import Foo from './Foo';
import { useState } from 'react';
function App() {
const [show, setShow] = useState(false);
return (
<>
<div>App</div>
<button onClick={() => setShow(true)}>显示</button>
{show && <Foo />}
</>
);
}
export default App;
使用 <Suspense>
可以在懒加载的组件加载时显示一个正在加载的提示,这个提示可以在控制台将网速设置较慢的时候可以看到
实现懒加载之后的代码如下:
import { useState, lazy, Suspense } from 'react';
const Foo = lazy(() => import('./Foo.jsx'));
function App() {
const [show, setShow] = useState(false);
return (
<>
<div>App</div>
<button onClick={() => setShow(true)}>显示</button>
<Suspense fallback={<div>Loading...</div>}>{show && <Foo />}</Suspense>
</>
);
}
export default App;