跳到主要内容

组件

创建组件的方式

创建组件有函数组件和类组件两种方式,React18 之后,全面使用函数组件,类组件会退出历史舞台

function Home(props) {
return <div className="home">Welcome to React~</div>;
}

const Home = () => {
return <div className="home">Welcome to React~</div>;
};

对组件的要求

  1. 组件名称必须以大写字母开头,否则 React 会将以小写字母开头的组件视为原生 DOM 标签
  2. 必须返回可以渲染的元素
    • 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(prevPerson => ({
...prevPerson,
artwork: {
...prevPerson.artwork,
city: 'beijing'
}
}));

更新数组:

  • 添加:setList([...list, 666])
  • 删除:通常使用 filter 方法生成一个不包含该元素的新数组
  • 更新:map()

Fragments

简单说就是避免向 DOM 中添加额外的节点。

假如有一个子组件<Columns />

const Columns = () => {
return (
<div>
<td>Hello</td>
<td>World</td>
</div>
);
};

有一个父组件使用了<Columns />

const Columns = () => {
return (
<table>
<tr>
<Columns />
</tr>
</table>
);
};

结果如下,在 tr 和 td 之间多了一个 div 节点,这样就导致了 html 是无效的

<table>
<tr>
<div>
<td>Hello</td>
<td>World</td>
</div>
</tr>
</table>

Fragments就解决了这个问题

const Columns = () => {
return (
<React.Fragment>
<td>Hello</td>
<td>World</td>
</React.Fragment>
);
};

也可以使用一种短语法,像空标签一样 <></>

const Columns = () => {
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>
);

作用:

  1. 检查组件是否是纯函数
  2. 及早发现 useEffect 中的错误
  3. 警告过时的 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 表单内置的受控组件的行为:

1、value + onChange

<input type="text" value={inputValue} onChange={handleChange} />

2、checked + onChange

<input type="checkbox" checked={checked} onChange={handleChange} />

受控组件的使用场景

运行前面的示例时,可以发现,非受控组件在初始打印一次 render,在输入时则不会打印,但是受控组件在输入时也会打印,说明受控组件会引起组件重新渲染。

受控组件的使用场景:

  1. 要处理输入的值
  2. 实时同步状态值到父组件

比如,在输入时,将输入的值转换为大写,如下示例:

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 组件获取

高阶组件 HOC

高阶组件(Higher-Order Component,HOC)是一个函数,指接收一个或多个组件作为参数,并返回一个新的组件。主要功能是封装并分离组件的通用逻辑。

高阶组件一般用 with 前缀命名

HOC 的作用:

  • 代码复用: 将公共的逻辑抽离出来形成 HOC,让多个组件复用
  • 功能增强: 为组件添加额外的功能,比如权限控制、数据获取等
  • 抽象复杂逻辑: 将复杂的逻辑封装在 HOC 中,简化组件的实现

使用场景:

  • 数据获取
  • 权限控制
  • 错误处理
  • 性能优化
  • 样式注入

注意:不要滥用 HOC

抽取重复逻辑

示例一,有 2 个组件都要判断用户是否登录,需要重复写判断代码:

function Foo() {
if (getToken()) {
return <div>foo ui</div>;
}
return null;
}

function Bar() {
if (getToken()) {
return <div>bar ui</div>;
}
return null;
}

可以将重复代码抽取出来,形成一个 HOC:

function withLogin(Component) {
const NewComponent = props => {
return getToken() ? <Component {...props} /> : null;
};
return NewComponent;
}

使用如下,以后如果要修改登录逻辑,也只需要在 HOC 中修改即可

const Foo = withLogin(props => {
return <div>foo ui</div>;
});
const Bar = withLogin(props => {
return <div>bar ui</div>;
});

示例二:获取用户权限

function withUserPermissions(WrappedComponent) {
return props => {
const permissions = getUserPermissions();
return <WrappedComponent {...props} permissions={permissions} />;
};
}

const EnhancedComponent = withUserPermissions(MyComponent);

嵌套调用

使用多个高阶组件嵌套来增强一个基础组件的功能。

const superX = withA(withB(withC(BaseComponent)));

示例,逐步添加增强功能,包括权限控制、日志记录、以及错误边界

import React, { useState, Suspense } from 'react';

// 基础组件
const BaseComponent = ({ text }) => {
return <div>{text}</div>;
};

// 权限控制
const withPermission = WrappedComponent => {
return props => {
const { hasPermission } = props;

if (!hasPermission) {
return <div>您没有权限访问此组件</div>;
}

return <WrappedComponent {...props} />;
};
};

// 日志记录
const withLogging = WrappedComponent => {
return props => {
console.log('组件挂载');

React.useEffect(() => {
return () => console.log('组件卸载');
}, []);

return <WrappedComponent {...props} />;
};
};

// 错误边界
const withErrorBoundary = WrappedComponent => {
return props => {
const [hasError, setHasError] = useState(false);

const handleError = (error, errorInfo) => {
console.error('组件出错:', error, errorInfo);
setHasError(true);
};

if (hasError) {
return <div>出错了,请稍后再试。</div>;
}

return (
<Suspense fallback={<div>加载中...</div>}>
<WrappedComponent {...props} onError={handleError} />
</Suspense>
);
};
};

const EnhancedComponent = withErrorBoundary(withLogging(withPermission(BaseComponent)));

function App() {
return (
<>
<div>
<h1>使用 HOC 增强的组件</h1>
<EnhancedComponent text="Hello, World!" hasPermission={true} />
</div>
</>
);
}

export default App;

也可以将多个 HOC 组合成一个 HOC。改造上面的代码,使用 compose 函数将多个函数组合成一个函数:

const compose = (...fns) => {
if (fns.length === 0) {
return arg => arg;
}
if (fns.length === 1) {
return fns[0];
}
return fns.reduce(
(a, b) =>
(...args) =>
a(b(...args))
);
};

const hoc = compose(withErrorBoundary, withLogging, withPermission);
const EnhancedComponent = hoc(BaseComponent);

Lazy 与 Suspense 实现懒加载

  • Lazy 能够让组件延迟加载,按需加载
  • Suspense 允许在子组件完成加载前展示备用方案
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>

示例 1:在 App.jsx 中引入 Foo 组件,但是并未使用,但是控制台会打印出 Foo。这个时候就没有做到按需加载

App.jsx
import Foo from './Foo';

function App() {
return (
<>
<div>App</div>
</>
);
}

export default App;
Foo.jsx
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;