Open WangShuXian6 opened 3 months ago
npm create vite@latest react-dp
react
typescript
pnpm i
pnpm i styled-components -S
pnpm run dev
src\components\split-screen.tsx
import React from "react";
import { styled } from "styled-components";
const Container = styled.div`
display: flex;
`;
const Panel = styled.div`
flex: 1;
`;
interface SplitScreenProps {
Left: React.ComponentType;
Right: React.ComponentType;
}
//使用 SplitScreenProps 作为 props 类型,并显式声明返回类型为 React.ReactElement。
export const SplitScreen = ({
Left,
Right,
}: SplitScreenProps): React.ReactElement => {
return (
<Container>
<Panel>
<Left />
</Panel>
<Panel>
<Right />
</Panel>
</Container>
);
};
src\App.tsx
import "./App.css";
import { SplitScreen } from "./components/split-screen";
const LeftSideComp = () => {
return <h2 style={{ backgroundColor: "red" }}>left</h2>;
};
const RightSideComp = () => {
return <h2 style={{ backgroundColor: "blue" }}>right</h2>;
};
function App() {
return <SplitScreen Left={LeftSideComp} Right={RightSideComp} />;
}
export default App;
src\components\split-screen.tsx
import React from 'react';
import { styled } from 'styled-components';
// 定义 styled-components 的类型
const Container = styled.div`
display: flex;
`;
interface PanelProps {
flex: number;
}
const Panel = styled.div<PanelProps>`
flex: ${(p) => p.flex};
`;
// 定义 SplitScreen 组件的 props 类型
interface SplitScreenProps {
children: [React.ReactNode, React.ReactNode];
leftWidth?: number;
rightWidth?: number;
}
export const SplitScreen = ({
children,
leftWidth = 1,
rightWidth = 1,
}: SplitScreenProps): React.ReactElement => {
const [left, right] = children;
return (
<Container>
<Panel flex={leftWidth}>{left}</Panel>
<Panel flex={rightWidth}>{right}</Panel>
</Container>
);
};
src\App.tsx
import React from 'react';
import './App.css';
import { SplitScreen } from './components/split-screen';
interface SideCompProps {
title: string;
}
const LeftSideComp = ({ title }: SideCompProps): React.ReactElement => {
return <h2 style={{ backgroundColor: 'crimson' }}>{title}</h2>;
};
const RightSideComp = ({ title }: SideCompProps): React.ReactElement => {
return <h2 style={{ backgroundColor: 'burlywood' }}>{title}</h2>;
};
function App(): React.ReactElement {
return (
<SplitScreen leftWidth={1} rightWidth={3}>
<LeftSideComp title="Left" />
<RightSideComp title="Right" />
</SplitScreen>
);
}
export default App;
src\components\authors\LargeListItems.tsx
import React from 'react';
export interface Author { name: string; age: number; country: string; books: string[]; }
interface LargeAuthorListItemProps { author: Author; }
export const LargeAuthorListItem = ({ author }: LargeAuthorListItemProps): React.ReactElement => { const { name, age, country, books } = author; return ( <>
<p>Age: {age}</p>
<p>Country: {country}</p>
<h2>Books</h2>
<ul>
{books.map((book) => (
<li key={book}>{book}</li>
))}
</ul>
</>
); };
>`src\components\authors\SmallListItems.tsx`
```tsx
// components/authors/SmallAuthorListItem.tsx
import React from 'react';
import { Author } from './LargeListItems';
interface SmallAuthorListItemProps {
author: Pick<Author, 'name' | 'age'>;
}
export const SmallAuthorListItem = ({ author }: SmallAuthorListItemProps): React.ReactElement => {
const { name, age } = author;
return (
<p>Name: {name}, Age: {age}</p>
);
};
src\components\lists\Regular.tsx
import React from 'react';
interface RegularListProps
export const RegularList = <T,>({ items, sourceName, ItemComponent }: RegularListProps
>`src\data\authors.ts`
```tsx
export const authors = [
{
name: "Sarah Waters",
age: 55,
country: "United Kingdom",
books: ["Fingersmith", "The Night Watch"],
},
{
name: "Haruki Murakami",
age: 71,
country: "Japan",
books: ["Norwegian Wood", "Kafka on the Shore"],
},
{
name: "Chimamanda Ngozi Adichie",
age: 43,
country: "Nigeria",
books: ["Half of a Yellow Sun", "Americanah"],
},
];
src\App.tsx
import { LargeAuthorListItem } from "./components/authors/LargeListItems"; import { SmallAuthorListItem } from "./components/authors/SmallListItems"; import { RegularList } from "./components/lists/Regular"; import { authors } from "./data/authors";
function App() { return ( <> <RegularList items={authors} sourceName={"author"} ItemComponent={SmallAuthorListItem} /> <RegularList items={authors} sourceName={"author"} ItemComponent={LargeAuthorListItem} /> </> ); }
export default App;
## 6. 列表类型 Lists Types
>`src\components\books\LargeListItems.tsx`
```tsx
import React from 'react';
export interface Book {
name: string;
price: number;
title: string;
pages: number;
}
interface LargeBookListItemProps {
book: Book;
}
export const LargeBookListItem = ({ book }: LargeBookListItemProps): React.ReactElement => {
const { name, price, title, pages } = book;
return (
<>
<h2>{name}</h2>
<p>{price}</p>
<h2>Title:</h2>
<p>{title}</p>
<p># of Pages: {pages}</p>
</>
);
};
src\components\books\SmallListItems.tsx
import React from 'react'; import { Book } from './LargeListItems';
interface SmallBookListItemProps { book: Pick<Book, 'name' | 'price'>; }
export const SmallBookListItem = ({ book }: SmallBookListItemProps): React.ReactElement => { const { name, price } = book; return (
); };
>`src\components\lists\Numbered.tsx`
```tsx
import React from "react";
interface NumberedListProps<T> {
items: T[];
sourceName: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ItemComponent: any; //React.ComponentType<{ [key: string]: T }>;
}
export const NumberedList = <T,>({
items,
sourceName,
ItemComponent,
}: NumberedListProps<T>): React.ReactElement => {
return (
<>
{items.map((item, i) => {
const props = { [sourceName]: item };
return (
<React.Fragment key={i}>
<h3>{i + 1}</h3>
<ItemComponent {...props} />
</React.Fragment>
);
})}
</>
);
};
src\App.tsx
import { LargeAuthorListItem } from "./components/authors/LargeListItems"; import { SmallAuthorListItem } from "./components/authors/SmallListItems"; import { LargeBookListItem } from "./components/books/LargeListItems"; import { SmallBookListItem } from "./components/books/SmallListItems"; import { NumberedList } from "./components/lists/Numbered"; import { RegularList } from "./components/lists/Regular"; import { authors } from "./data/authors"; import { books } from "./data/books";
function App() { return ( <> <RegularList items={authors} sourceName={"author"} ItemComponent={SmallAuthorListItem} /> <NumberedList items={authors} sourceName={"author"} ItemComponent={LargeAuthorListItem} />
<RegularList
items={books}
sourceName={"book"}
ItemComponent={SmallBookListItem}
/>
<NumberedList
items={books}
sourceName={"book"}
ItemComponent={LargeBookListItem}
/>
</>
); }
export default App;
## 7. 模态框 Modals
非受控,因为父级无法从模态框外部控制模态框的状态。
这个模型是非受控的,因为这个模型本身可以控制自己,比如显示和隐藏组件的 show 和 setShow。
我们说它是非受控的,因为外部组件无法直接访问它的特性。
因为我无法访问这个模型的状态,包括 show 和 setShow,这降低了模型的灵活性,因为它是非受控的。
>`src\components\Modal.tsx`
```tsx
import React, { useState, ReactNode } from 'react';
import { styled } from 'styled-components';
const ModalBackground = styled.div`
position: absolute;
left: 0;
top: 0;
overflow: auto;
background-color: #00000067;
width: 100%;
height: 100%;
`;
const ModalContent = styled.div`
margin: 12% auto;
padding: 24px;
background-color: wheat;
width: 50%;
`;
interface ModalProps {
children: ReactNode;
}
export const Modal = ({ children }: ModalProps): React.ReactElement => {
const [show, setShow] = useState<boolean>(false);
return (
<>
<button onClick={() => setShow(true)}>Show Modal</button>
{show && (
<ModalBackground onClick={() => setShow(false)}>
<ModalContent onClick={(e) => e.stopPropagation()}>
<button onClick={() => setShow(false)}>Hide Modal</button>
{children}
</ModalContent>
</ModalBackground>
)}
</>
);
};
src\App.tsx
import { Modal } from "./components/Modal"; import { LargeBookListItem } from "./components/books/LargeListItems"; import { books } from "./data/books";
function App() { return ( <>
</>
); }
export default App;
![图片](https://github.com/WangShuXian6/blog/assets/30850497/19756a09-ae28-4531-aa04-6862257575c0)
从某种意义上说,容器组件是负责数据加载和数据管理的React组件,它们为子组件处理这些任务。 这里显示的是容器组件包裹多个子组件的情况。
通常,如果你是一个初级或中级的React开发者,可能会让子组件自行加载数据并独立显示。
例如,你可能会使用Usestate和Useeffect钩子以及像Axios或Fetch这样的库来从服务器获取数据。
然而,当多个子组件需要共享相同的数据加载逻辑时,就会出现问题。
这时,容器组件就派上用场了。
它们通过将数据加载逻辑提取到一个专门的组件中来解决这个问题。
容器组件负责数据检索过程,并将数据自动传递给子组件。
很快我们将深入探讨容器组件如何实现这一点。
但在此之前,让我们先了解容器组件背后的核心概念,类似于布局组件,我们旨在让子组件不必了解它们所处的特定布局。
容器组件遵循类似的原则。
我们希望组件不知道其数据的来源或管理方式。
相反,它们只需接收props并显示相关内容,而无需了解底层的数据处理。
pnpm i express -D
server.js
//const express = require("express");
import express from 'express';
const app = express();
app.use(express.json());
let currentUser = {
name: "Sarah Waters",
age: 55,
country: "United Kingdom",
books: ["Fingersmith", "The Night Watch"],
};
let users = [
{
name: "Sarah Waters",
age: 55,
country: "United Kingdom",
books: ["Fingersmith", "The Night Watch"],
},
{
name: "Haruki Murakami",
age: 71,
country: "Japan",
books: ["Norwegian Wood", "Kafka on the Shore"],
},
{
name: "Chimamanda Ngozi Adichie",
age: 43,
country: "Nigeria",
books: ["Half of a Yellow Sun", "Americanah"],
},
];
let books = [
{
name: "To Kill a Mockingbird",
pages: 281,
title: "Harper Lee",
price: 12.99,
},
{
name: "The Catcher in the Rye",
pages: 224,
title: "J.D. Salinger",
price: 9.99,
},
{
name: "The Little Prince",
pages: 85,
title: "Antoine de Saint-Exupéry",
price: 7.99,
},
];
app.get("/current-user", (req, res) => res.json(currentUser));
app.get("/users/:id", (req, res) => {
const { id } = req.params;
console.log(id);
res.json(users.find((user) => user.id === id));
});
app.get("/users", (req, res) => res.json(users));
app.post("/users/:id", (req, res) => {
const { id } = req.params;
const { user: editedUser } = req.body;
users = users.map((user) => (user.id === id ? editedUser : user));
res.json(users.find((user) => user.id === id));
});
app.get("/books", (req, res) => res.json(books));
app.get("/books/:id", (req, res) => {
const { id } = req.params;
res.json(books.find((book) => book.id === id));
});
let SERVER_PORT = 9090;
app.listen(SERVER_PORT, () =>
console.log(`Server is listening on port: ${SERVER_PORT}`)
);
package.json
"proxy": "http://localhost:9090",
{
"name": "react-design-patterns",
"version": "0.1.0",
"private": true,
"proxy": "http://localhost:9090",
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"styled-components": "^6.0.0-rc.3",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/node_modules/**": true
},
"devDependencies": {
"express": "^4.19.2"
}
}
请求
const response = await axios.get('/current-user');
配置 vite.config.ts 以设置代理服务器,将前端请求代理到后端 Express 服务器。
在这里,/api 前缀会被代理到
http://localhost:9090
,并且会去掉 /api 前缀。这意味着当你在前端发出/api/current-user
请求时,它会被代理到http://localhost:9090/current-user
。通过配置 Vite 的代理设置和在前端使用相对路径来发送 API 请求,我们可以实现前端与后端的通信。这样可以避免跨域问题,并且使得开发环境配置更加简洁。
vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:9090',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
});
请求
const response = await axios.get('/api/current-user');
node server.js
pnpm i axios -S
src\components\user-info.tsx
import React from 'react';
// 定义 User 类型
type User = {
name: string;
age: number;
country: string;
books: string[];
};
// 定义组件的 Props 类型
type UserInfoProps = {
user?: User;
};
// 使用 FC 和 Props 类型定义组件
export const UserInfo= ({ user }:UserInfoProps) : React.ReactElement=> {
const { name, age, country, books } = user || {} as User;
return user ? (
<>
<h2>{name}</h2>
<p>Age: {age} years</p>
<p>Country: {country}</p>
<h2>Books</h2>
<ul>
{books.map((book) => (
<li key={book}>{book}</li>
))}
</ul>
</>
) : (
<h1>Loading...</h1>
);
};
容器组件 current-user-loader 将数据传递给子组件 user-info
current-user-loader.tsx
import axios from "axios";
import React, { useEffect, useState, ReactElement } from "react";
// 定义 User 类型
type User = {
name: string;
age: number;
country: string;
books: string[];
};
// 定义组件的 Props 类型
type CurrentUserLoaderProps = {
children: ReactElement<{ user: User | null }>;
};
// 使用 FC 和 Props 类型定义组件
export const CurrentUserLoader = ({
children,
}: CurrentUserLoaderProps): React.ReactElement => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
(async () => {
const response = await axios.get("/api/current-user");
setUser(response.data);
})();
}, []);
return <>{React.cloneElement(children, { user })}</>;
};
src\App.tsx
import { CurrentUserLoader } from "./components/current-user-loader";
import { UserInfo } from "./components/user-info";
function App() {
return (
<>
<CurrentUserLoader>
<UserInfo />
</CurrentUserLoader>
</>
);
}
export default App;
之前的组件,只能获取当前用户的数据。 也许我们想根据 ID 获取用户的数据。使其更加通用。
server.js
//const express = require("express");
import express from 'express';
const app = express();
app.use(express.json());
let currentUser = {
id: "1",
name: "Sarah Waters",
age: 55,
country: "United Kingdom",
books: ["Fingersmith", "The Night Watch"],
};
let users = [
{
id: "1",
name: "Sarah Waters",
age: 55,
country: "United Kingdom",
books: ["Fingersmith", "The Night Watch"],
},
{
id: "2",
name: "Haruki Murakami",
age: 71,
country: "Japan",
books: ["Norwegian Wood", "Kafka on the Shore"],
},
{
id: "3",
name: "Chimamanda Ngozi Adichie",
age: 43,
country: "Nigeria",
books: ["Half of a Yellow Sun", "Americanah"],
},
];
let books = [
{
id: "1",
name: "To Kill a Mockingbird",
pages: 281,
title: "Harper Lee",
price: 12.99,
},
{
id: "2",
name: "The Catcher in the Rye",
pages: 224,
title: "J.D. Salinger",
price: 9.99,
},
{
id: "3",
name: "The Little Prince",
pages: 85,
title: "Antoine de Saint-Exupéry",
price: 7.99,
},
];
app.get("/current-user", (req, res) => res.json(currentUser));
app.get("/users/:id", (req, res) => {
const { id } = req.params;
res.json(users.find((user) => user.id === id));
});
app.get("/users", (req, res) => res.json(users));
app.post("/users/:id", (req, res) => {
const { id } = req.params;
const { user: editedUser } = req.body;
users = users.map((user) => (user.id === id ? editedUser : user));
res.json(users.find((user) => user.id === id));
});
app.get("/books", (req, res) => res.json(books));
app.get("/books/:id", (req, res) => {
const { id } = req.params;
res.json(books.find((book) => book.id === id));
});
let SERVER_PORT = 9090;
app.listen(SERVER_PORT, () =>
console.log(`Server is listening on port: ${SERVER_PORT}`)
);
src\components\user-loader.tsx
import axios from "axios";
import React, { useEffect, useState, ReactElement } from "react";
// 定义 User 类型
type User = {
name: string;
age: number;
country: string;
books: string[];
};
// 定义 UserLoaderProps 类型
type UserLoaderProps = {
userId: string;
children: ReactElement<{ user: User | null }>;
};
// `UserLoader` 组件
export const UserLoader = ({
userId,
children,
}: UserLoaderProps): ReactElement => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
(async () => {
const response = await axios.get(`/api/users/${userId}`);
setUser(response.data);
})();
}, [userId]);
return (
<>
{React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, { user });
}
return child;
})}
</>
);
};
import { UserInfo } from "./components/user-info";
import { UserLoader } from "./components/user-loader";
function App() {
return (
<>
<UserLoader userId={"1"}>
<UserInfo />
</UserLoader>
<UserLoader userId={"2"}>
<UserInfo />
</UserLoader>
<UserLoader userId={"3"}>
<UserInfo />
</UserLoader>
</>
);
}
export default App;
通用资源数据获取容器,通过动态api和动态子组件属性,为任意子组件获取数据
src\components\book-info.tsx
import React from 'react';
type Book = {
name: string;
price: number;
title: string;
pages: number;
};
type BookInfoProps = {
book?: Book;
};
export const BookInfo = ({ book }: BookInfoProps): React.ReactElement => {
const { name, price, title, pages } = book || {} as Book;
return book ? (
<>
<h3>{name}</h3>
<p>{price}</p>
<h3>Title: {title}</h3>
<p>Number of Pages: {pages}</p>
</>
) : (
<h1>Loading</h1>
);
};
src\components\user-info.tsx
import React from 'react';
// 定义 User 类型
type User = {
name: string;
age: number;
country: string;
books: string[];
};
// 定义组件的 Props 类型
type UserInfoProps = {
user?: User;
};
export const UserInfo= ({ user }:UserInfoProps) : React.ReactElement=> {
const { name, age, country, books } = user || {} as User;
return user ? (
<>
<h2>{name}</h2>
<p>Age: {age} years</p>
<p>Country: {country}</p>
<h2>Books</h2>
<ul>
{books.map((book) => (
<li key={book}>{book}</li>
))}
</ul>
</>
) : (
<h1>Loading...</h1>
);
};
src\components\resource-loader.tsx
import axios from "axios";
import React, {
useEffect,
useState,
ReactNode,
ReactElement,
cloneElement,
} from "react";
type ResourceLoaderProps = {
resourceUrl: string;
resourceName: string;
children: ReactNode;
};
export const ResourceLoader = ({
resourceUrl,
resourceName,
children,
}: ResourceLoaderProps): ReactElement => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [resource, setResource] = useState<any>(null);
useEffect(() => {
(async () => {
const response = await axios.get(resourceUrl);
setResource(response.data);
})();
}, [resourceUrl]);
return (
<>
{React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return cloneElement(child, { [resourceName]: resource });
}
return child;
})}
</>
);
};
src\App.tsx
import { BookInfo } from "./components/book-info";
import { UserInfo } from "./components/user-info";
import { ResourceLoader } from "./components/resource-loader";
function App() {
return (
<>
<ResourceLoader resourceUrl={"/api/users/1"} resourceName={"user"}>
<UserInfo />
</ResourceLoader>
<ResourceLoader resourceUrl={"/api/books/1"} resourceName={"book"}>
<BookInfo />
</ResourceLoader>
</>
);
}
export default App;
更通用的资源加载容器,无需关心是否有请求功能,无需关心数据源。只负责传递数据给子组件。
src\components\data-source.tsx
import React, {
useEffect,
useState,
ReactNode,
ReactElement,
cloneElement,
} from "react";
type DataSourceProps<T> = {
getData: () => Promise<T>;
resourceName: string;
children: ReactNode;
};
export const DataSource = <T,>({
getData,
resourceName,
children,
}: DataSourceProps<T>): ReactElement => {
const [resource, setResource] = useState<T | null>(null);
useEffect(() => {
(async () => {
const data = await getData();
setResource(data);
})();
}, [getData]);
return (
<>
{React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return cloneElement(child, { [resourceName]: resource });
}
return child;
})}
</>
);
};
src\App.tsx
import axios from "axios";
import { DataSource } from "./components/data-source";
import { UserInfo, type User } from "./components/user-info";
const fetchData = async <T,>(url: string): Promise<T> => {
const response = await axios.get(url);
return response.data;
};
function App() {
return (
<>
<DataSource
getData={() => fetchData<User>("/api/users/1")}
resourceName="user"
>
<UserInfo />
</DataSource>
</>
);
}
export default App;
注意,不应该在简单组件中使用 cloneElement 克隆元素传递数据,因为它们会导致可维护性降低。
所以使用渲染属性[render]模式的容器组件来传递数据,替代 cloneElement
src\components\data-source-with-render-props.tsx
import React, { useEffect, useState, ReactNode } from "react";
type DataSourceWithRenderProps<T> = {
getData: () => Promise<T>;
render: (resource?: T) => ReactNode;
};
export const DataSourceWithRenderProps = <T,>({
getData,
render,
}: DataSourceWithRenderProps<T>) => {
const [resource, setResource] = useState<T>();
useEffect(() => {
(async () => {
const data = await getData();
setResource(data);
})();
}, [getData]);
return <>{render(resource)}</>;
};
src\components\user-info.tsx
import React from 'react';
// 定义 User 类型
export type User = {
name: string;
age: number;
country: string;
books: string[];
};
// 定义组件的 Props 类型
type UserInfoProps = {
user?: User;
};
export const UserInfo= ({ user }:UserInfoProps) : React.ReactElement=> {
const { name, age, country, books } = user || {} as User;
return user ? (
<>
<h2>{name}</h2>
<p>Age: {age} years</p>
<p>Country: {country}</p>
<h2>Books</h2>
<ul>
{books.map((book) => (
<li key={book}>{book}</li>
))}
</ul>
</>
) : (
<h1>Loading...</h1>
);
};
src\App.tsx
import axios from "axios";
import { DataSourceWithRenderProps } from "./components/data-source-with-render-props";
import { UserInfo, type User } from "./components/user-info";
const fetchData = async <T,>(url: string): Promise<T> => {
const response = await axios.get(url);
return response.data;
};
function App() {
return (
<>
<DataSourceWithRenderProps<User>
getData={() => fetchData<User>("/api/users/1")}
render={(resource) => <UserInfo user={resource } />}
/>
</>
);
}
export default App;
src\App.tsx
import axios from "axios";
import { DataSource } from "./components/data-source";
import { UserInfo, type User } from "./components/user-info";
const fetchData = async <T,>(url: string): Promise<T> => {
const response = await axios.get(url);
return response.data;
};
const getDataFromLocalStorage = (key: string) => (): string | null => {
return localStorage.getItem(key);
};
type MessageProps = {
msg?: string ;
};
const Message = ({ msg }: MessageProps): React.ReactElement => <h1>{msg}</h1>;
function App() {
return (
<>
<DataSource
getData={() => fetchData("/api/users/1")}
resourceName={"user"}
>
<UserInfo />
</DataSource>
<DataSource
getData={async () => getDataFromLocalStorage("test")}
resourceName={"msg"}
>
<Message />
</DataSource>
</>
);
}
export default App;
在本章中,我们将探讨一个基本的 React 设计模式:受控和非受控组件。
这些模式在 React 中非常常见,因此理解它们的区别和使用场景是至关重要的。
让我们先了解一下 React 中的非受控组件。
非受控组件是指组件自身管理其内部状态,组件内的数据通常仅在特定事件发生时被访问。
一个常见的例子是非受控表单,表单输入的值只有在用户触发提交事件时才能被外部组件知道。
另一方面,受控组件是指父组件负责管理状态,然后将状态传递给受控组件作为属性。
父组件处理状态并控制受控组件的行为。
这些是受控和非受控组件的基本定义。
在非受控组件中,组件本身通常使用像 useState
这样的钩子来管理自己的状态。
在这里提供的代码片段中,我们可以看到一个使用 useState
钩子的非受控组件。
传递给这个组件的唯一属性是 onSubmit
,这是由父组件提供的一个函数,用于在提交事件发生时检索内部状态的值。
在受控组件中,组件的状态不再由组件本身管理。
相反,状态是作为属性从父组件传递下来的。
在给出的示例中,你会注意到受控组件不再使用 useState
钩子。
状态是作为属性从父组件接收的,并且相应地使用了额外的函数。
在本章中,我们将很快查看受控和非受控组件的具体示例。
现在一个常见的问题是,我们应该更倾向于使用哪种方式,受控组件还是非受控组件?
在大多数情况下,受控组件是首选。
这种偏好的原因有几个。
首先,受控组件更易用,也更易于测试。
使用受控组件,我们可以轻松设置所需状态的组件以进行测试。
这消除了手动操作组件和触发事件以检查其内部行为的需求。
我们将创建一个非受控表单,所以我们称之为 UncontrolledForm.js。
正如我所说的,非受控组件或像这里的表单这样的元素是一种不会泄露其状态的元素或组件。
所以我们无法使用任何 useState 或钩子来访问这个表单的元素状态。
我们将使用实际的 DOM 来访问它们,例如使用 createRef 函数等。
由于这是一个非受控表单,我们必须使用 React.createRef 来访问这些元素。
为了防止表单提交时页面刷新,我们使用 e.preventDefault()。
由于这个表单是非受控的,它的状态和特性对外部组件是不可访问的。
因此,我们必须使用 createRef 这样的间接方法来访问它的特性,这就是所谓的非受控表单。
总之,只有当我们提交这个表单时,它的数据才会被组件外部所改变。
src\components\uncontrolled-form.tsx
import React, { FormEvent } from "react";
export const UncontrolledForm = (): React.ReactElement => {
const nameInputRef = React.createRef<HTMLInputElement>();
const ageInputRef = React.createRef<HTMLInputElement>();
const SubmitForm = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault();
if (nameInputRef.current && ageInputRef.current) {
console.log(nameInputRef.current.value);
console.log(ageInputRef.current.value);
}
};
return (
<form onSubmit={SubmitForm}>
<input name="name" type="text" placeholder="Name" ref={nameInputRef} />
<input name="age" type="number" placeholder="Age" ref={ageInputRef} />
<input type="submit" value="Submit" />
</form>
);
};
src\App.tsx
import { UncontrolledForm } from "./components/uncontrolled-form";
function App() {
return (
<>
<UncontrolledForm />
</>
);
}
export default App;
可以为组件添加额外功能,例如验证。
要创建的这个受控表单,它的基本区别在于,我们将使用像 useState 和 useEffect 这样的钩子来跟踪用户在表单中输入的值。
为了跟踪表单的输入,我们需要为每个输入创建一个状态。
除了不再需要 ref 之外,其他都保持不变。
为了美观,我们添加一个按钮,因为我们不依赖于表单的 onSubmit 事件。
现在我们有了一个受控表单。
它的状态可以直接从外部跟踪。
其中一个好处是,例如,如果你需要在用户输入之前进行一些输入验证,你可以更容易地做到这一点。
为此,我们在其中添加一个 useEffect。
我们检查姓名长度是否小于1,也就是输入为空。
当添加一些功能时,受控表单比非受控表单更加灵活。
src\components\controlled-form.tsx
import React, { useEffect, useState, ChangeEvent, ReactElement } from "react";
// 定义 ControlledForm 组件
export const ControlledForm = (): ReactElement => {
const [error, setError] = useState<string>("");
const [name, setName] = useState<string>("");
const [age, setAge] = useState<number | undefined>();
useEffect(() => {
if (name.length < 1) {
setError("The name can not be empty");
} else {
setError("");
}
}, [name]);
const handleNameChange = (e: ChangeEvent<HTMLInputElement>): void => {
setName(e.target.value);
};
const handleAgeChange = (e: ChangeEvent<HTMLInputElement>): void => {
const value = e.target.value;
setAge(value === "" ? undefined : parseInt(value, 10));
};
return (
<form>
{error && <p>{error}</p>}
<input
name="name"
type="text"
placeholder="Name"
value={name}
onChange={handleNameChange}
/>
<input
name="age"
type="number"
placeholder="Age"
value={age === undefined ? "" : age}
onChange={handleAgeChange}
/>
<button type="submit">Submit</button>
</form>
);
};
src\App.tsx
import { ControlledForm } from "./components/controlled-form";
function App() {
return (
<>
<ControlledForm />
</>
);
}
export default App;
不再在内部更改它的状态(显示或隐藏),而是将其移到 App 组件中处理。
这是受控模型组件,因为它的状态将由外部控制。
onClose 不是在内部定义的,而是在外部。
一些特性如 shouldDisplay 也是从外部传入的。
它从 false 到 true,再从 true 到 false 的触发在父组件中进行,而不是在组件内部。
这就是为什么我们称它为受控组件。
它的状态 shouldDisplay 和 setShouldDisplay 将由 App 组件控制。
对于受控模型,我们要传递 shouldDisplay。
这样它可以用来显示自己 shouldDisplay。
还有用于关闭的 onClose,因为这是一个属性函数。
基本上,这就是受控模型,因为它不控制自己的状态。
相反,容器或父组件(即这里的 App 组件)控制它的状态。
src\components\controlled-modal.tsx
import React, { ReactNode } from "react";
import styled from "styled-components";
const ModalBackground = styled.div`
position: absolute;
left: 0;
top: 0;
overflow: auto;
background-color: #00000067;
width: 100%;
height: 100%;
`;
const ModalContent = styled.div`
margin: 12% auto;
padding: 24px;
background-color: wheat;
width: 50%;
`;
type ControlledModalProps = {
shouldShow: boolean;
close: () => void;
children: ReactNode;
};
export const ControlledModal = ({
shouldShow,
close,
children
}: ControlledModalProps): React.ReactElement | null => {
return (
<>
{shouldShow && (
<ModalBackground onClick={close}>
<ModalContent onClick={(e) => e.stopPropagation()}>
<button onClick={close}>Hide Modal</button>
{children}
</ModalContent>
</ModalBackground>
)}
</>
);
};
src\App.tsx
import { useState } from "react";
import { ControlledModal } from "./components/controlled-modal";
function App() {
const [showModal, setShowModal] = useState(false);
return (
<>
<button onClick={() => setShowModal(!showModal)}>
{" "}
{showModal ? "Hide Modal" : "Show Modal"}{" "}
</button>
<ControlledModal shouldShow={showModal} close={() => setShowModal(false)}>
<h1>I am the body of the modal!</h1>
</ControlledModal>
</>
);
}
export default App;
src\components\uncontrolled-flow.tsx
import React, { useState, ReactElement, ReactNode } from "react";
type UncontrolledFlowProps = {
children: ReactNode;
onDone?: () => void;
};
type StepProps = {
next: () => void;
};
export const UncontrolledFlow = ({
children,
onDone,
}: UncontrolledFlowProps): ReactElement => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [data, setData] = useState<Record<string, any>>({});
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const childrenArray = React.Children.toArray(children);
const currentChild = childrenArray[currentStepIndex];
const next = () => {
if (currentStepIndex < childrenArray.length - 1) {
setCurrentStepIndex(currentStepIndex + 1);
} else if (onDone) {
onDone();
}
};
if (React.isValidElement<StepProps>(currentChild)) {
return React.cloneElement(currentChild, { next });
}
return currentChild as ReactElement;
};
src\components\Steps.tsx
import React from "react";
type StepProps = {
next?: () => void;
};
const StepOne = ({ next }: StepProps): React.ReactElement => {
return (
<>
<h1>Step #1</h1>
<button onClick={next}>Next</button>
</>
);
};
const StepTwo = ({ next }: StepProps): React.ReactElement => {
return (
<>
<h1>Step #2</h1>
<button onClick={next}>Next</button>
</>
);
};
const StepThree = ({ next }: StepProps): React.ReactElement => {
return (
<>
<h1>Step #3</h1>
<button onClick={next}>Next</button>
</>
);
};
export { StepOne, StepTwo, StepThree };
src\App.tsx
import React from "react";
import { UncontrolledFlow } from "./components/uncontrolled-flow";
import { StepOne, StepTwo, StepThree } from "./components/Steps";
function App(): React.ReactElement {
return (
<>
<UncontrolledFlow>
<StepOne />
<StepTwo />
<StepThree />
</UncontrolledFlow>
</>
);
}
export default App;
src\components\uncontrolled-flow.tsx
import React, { useState, ReactElement, ReactNode } from "react";
type UncontrolledFlowProps = {
children: ReactNode;
onDone: (data: Record<string, any>) => void;
};
type StepProps = {
next: (dataFromStep: Record<string, any>) => void;
};
export const UncontrolledFlow = ({
children,
onDone,
}: UncontrolledFlowProps): ReactElement => {
const [data, setData] = useState<Record<string, any>>({});
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const childrenArray = React.Children.toArray(children);
const currentChild = childrenArray[currentStepIndex];
const next = (dataFromStep: Record<string, any>) => {
const nextIndex = currentStepIndex + 1;
const updatedData = { ...data, ...dataFromStep };
console.log(updatedData);
if (nextIndex < childrenArray.length) {
setCurrentStepIndex(nextIndex);
} else {
onDone(updatedData);
}
setData(updatedData);
};
if (React.isValidElement<StepProps>(currentChild)) {
return React.cloneElement(currentChild, { next });
}
return currentChild as ReactElement;
};
src\components\Steps.tsx
import React from "react";
type StepProps = {
next: (dataFromStep: Record<string, any>) => void;
};
const StepOne = ({ next }: StepProps): React.ReactElement => {
return (
<>
<h1>Step #1: Enter your name</h1>
<button onClick={() => next({ name: "TestName" })}>Next</button>
</>
);
};
const StepTwo = ({ next }: StepProps): React.ReactElement => {
return (
<>
<h1>Step #2: Enter your age</h1>
<button onClick={() => next({ age: 23 })}>Next</button>
</>
);
};
const StepThree = ({ next }: StepProps): React.ReactElement => {
return (
<>
<h1>Step #3: Enter your country</h1>
<button onClick={() => next({ country: "Poland" })}>Next</button>
</>
);
};
export { StepOne, StepTwo, StepThree };
src\App.tsx
import React from "react";
import { UncontrolledFlow } from "./components/uncontrolled-flow";
import { StepOne, StepTwo, StepThree } from "./components/Steps";
function App(): React.ReactElement {
return (
<>
<UncontrolledFlow
onDone={(data) => {
console.log(data);
alert("Onboarding Flow Done!");
}}
>
<StepOne next={()=>{}}/>
<StepTwo next={()=>{}}/>
<StepThree next={()=>{}}/>
</UncontrolledFlow>
</>
);
}
export default App;
src\components\controlled-flow.tsx
import React, { ReactElement, ReactNode } from "react";
type ControlledFlowProps = {
children: ReactNode;
onDone?: (data: Record<string, any>) => void;
currentStepIndex: number;
onNext: (data: Record<string, any>) => void;
};
type StepProps = {
next: (data: Record<string, any>) => void;
};
export const ControlledFlow = ({
children,
onDone,
currentStepIndex,
onNext,
}: ControlledFlowProps): ReactElement => {
const next = (data: Record<string, any>) => {
onNext(data);
};
const currentChild = React.Children.toArray(children)[currentStepIndex];
if (React.isValidElement<StepProps>(currentChild)) {
return React.cloneElement(currentChild, { next });
}
return currentChild as ReactElement;
};
src\components\Steps.tsx
import React from "react";
type StepProps = {
next: (data: Record<string, any>) => void;
};
const StepOne = ({ next }: StepProps): React.ReactElement => {
return (
<>
<h1>Step #1: Enter your name</h1>
<button onClick={() => next({ name: "TestName" })}>Next</button>
</>
);
};
const StepTwo = ({ next }: StepProps): React.ReactElement => {
return (
<>
<h1>Step #2: Enter your age</h1>
<button onClick={() => next({ age: 30 })}>Next</button>
</>
);
};
const StepThree = ({ next }: StepProps): React.ReactElement => {
return (
<>
<h1>Step #3: You qualify!</h1>
<button onClick={() => next({})}>Next</button>
</>
);
};
const StepFour = ({ next }: StepProps): React.ReactElement => {
return (
<>
<h1>Step #4: Enter your country</h1>
<button onClick={() => next({ country: "Poland" })}>Next</button>
</>
);
};
export { StepOne, StepTwo, StepThree, StepFour };
src\App.tsx
import React, { useState } from "react";
import { ControlledFlow } from "./components/controlled-flow";
import { StepOne, StepTwo, StepThree, StepFour } from "./components/Steps";
function App(): React.ReactElement {
const [data, setData] = useState<Record<string, any>>({});
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const next = (dataFromStep: Record<string, any>) => {
setData((prevData) => ({ ...prevData, ...dataFromStep }));
setCurrentStepIndex(currentStepIndex + 1);
};
return (
<>
<ControlledFlow currentStepIndex={currentStepIndex} onNext={next}>
<StepOne next={()=>{}}/>
<StepTwo next={()=>{}}/>
{data.age > 25 && <StepThree next={()=>{}}/>}
<StepFour next={()=>{}}/>
</ControlledFlow>
</>
);
}
export default App;
React设计模式:高阶组件。
高阶组件(简称HOC)是一些组件,它们不是直接返回JSX,而是返回另一个组件。
大多数React组件只是返回JSX,这些JSX代表将要渲染的DOM元素。
然而,通过高阶组件,我们引入了一个额外的层次,HOC不会直接返回JSX,而是返回另一个组件,这个组件再返回JSX。
为了简化这个概念,记住高阶组件本质上是返回组件的函数。
你可以把它们看作是组件工厂,当这些函数被调用时,它们会生成新的组件。
这种思维模型将帮助你掌握HOC的本质。
那么,为什么要创建高阶组件呢?
原因有几个。首先,HOC使我们能够在多个组件之间共享行为。
这类似于我们在容器组件中看到的,不同的组件被包装在同一个容器中,并表现出相似的行为。
高阶组件提供了一种实现类似功能的方法,用于共享相关的逻辑。
此外,高阶组件允许我们为现有组件添加额外功能。
如果我们遇到一个现有的组件,比如由其他人开发的遗留代码,HOC提供了一种方法,可以在不修改原始代码的情况下,为该组件增加新的功能和特性。
在本章的示例中,我们将更详细地探讨这些情况,展示高阶组件如何增强代码重用性和扩展组件功能。
src\components\check-props.tsx
import React from 'react';
export const checkProps =
(Component: React.ComponentType
) => { return (props: P) => { console.log(props); return <Component {...props} />; }; };
> `src\components\user-info.tsx`
```tsx
import React from 'react';
// 定义 User 类型
export type User = {
name: string;
age: number;
country: string;
books: string[];
};
// 定义组件的 Props 类型
export type UserInfoProps = {
user?: User;
};
export const UserInfo= ({ user }:UserInfoProps) : React.ReactElement=> {
const { name, age, country, books } = user || {} as User;
return user ? (
<>
<h2>{name}</h2>
<p>Age: {age} years</p>
<p>Country: {country}</p>
<h2>Books</h2>
<ul>
{books.map((book) => (
<li key={book}>{book}</li>
))}
</ul>
</>
) : (
<h1>Loading...</h1>
);
};
src\App.tsx
import React from 'react'; import { checkProps } from './components/check-props'; import { UserInfo, type UserInfoProps } from './components/user-info';
const UserInfoWrapper = checkProps
function App() { return ( <> <UserInfoWrapper user={{ name: "Sarah Waters", age: 55, country: "United Kingdom", books: ["Fingersmith", "The Night Watch"] }} /> </> ); }
export default App;
![图片](https://github.com/WangShuXian6/blog/assets/30850497/509951bc-9f85-4ecb-816a-ad3a2f87e80a)
## 3. 使用高阶组件加载数据 Data Loading with HOC
>`src\components\include-user.tsx`
```tsx
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { User } from './user-info';
export const includeUser = <P extends object>(Component: React.ComponentType<P & { user?: User }>, userId: string) => {
return (props: P) => {
const [user, setUser] = useState<User | undefined>(undefined);
useEffect(() => {
const fetchUser = async () => {
try {
const response = await axios.get(`/api/users/${userId}`);
setUser(response.data);
} catch (error) {
console.error('获取用户数据时出错:', error);
setUser(undefined); // 或者根据需要处理错误状态
}
};
fetchUser();
}, []); // 不依赖于 userId
return <Component {...props} user={user} />;
};
};
src\App.tsx
import { includeUser } from "./components/include-user"; import { UserInfo } from "./components/user-info";
const UserInfoWithUser = includeUser(UserInfo, "2");
function App() { return ( <>
</>
); }
export default App;
### 4 使用高阶组件更新数据 Updating Data with HOC
>`src\components\include-updatable-user.tsx`
```tsx
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { User } from './user-info';
type IncludeUpdatableUserProps = {
updatableUser: User | null;
changeHandler: (updates: Partial<User>) => void;
userPostHandler: () => Promise<void>;
resetUserHandler: () => void;
};
export const includeUpdatableUser = <P extends object>(Component: React.ComponentType<P & IncludeUpdatableUserProps>, userId: string) => {
return (props: P) => {
const [user, setUser] = useState<User | null>(null);
const [updatableUser, setUpdatableUser] = useState<User | null>(null);
useEffect(() => {
(async () => {
const response = await axios.get(`/api/users/${userId}`);
setUser(response.data);
setUpdatableUser(response.data);
})();
}, [userId]);
const userChangeHandler = (updates: Partial<User>) => {
setUpdatableUser((prev) => (prev ? { ...prev, ...updates } : null));
};
const userPostHandler = async () => {
if (updatableUser) {
const response = await axios.post(`/api/users/${userId}`, {
user: updatableUser,
});
setUser(response.data);
setUpdatableUser(response.data);
}
};
const resetUserHandler = () => {
setUpdatableUser(user);
};
return (
<Component
{...props}
updatableUser={updatableUser}
changeHandler={userChangeHandler}
userPostHandler={userPostHandler}
resetUserHandler={resetUserHandler}
/>
);
};
};
src\components\user-form.tsx
import React from 'react'; import { includeUpdatableUser } from './include-updatable-user'; import { User } from './user-info';
type UserInfoFormProps = {
updatableUser: User | null;
changeHandler: (updates: Partial
export const UserInfoForm = includeUpdatableUser( ({ updatableUser, changeHandler, userPostHandler, resetUserHandler }: UserInfoFormProps) => { const { name, age } = updatableUser || {};
return updatableUser ? (
<>
<label>
Name:
<input
value={name}
onChange={(e) => changeHandler({ name: e.target.value })}
/>
</label>
<label>
Age:
<input
value={age}
onChange={(e) => changeHandler({ age: Number(e.target.value) })}
/>
</label>
<button onClick={resetUserHandler}>Reset</button>
<button onClick={userPostHandler}>Save</button>
</>
) : (
<h3>Loading...</h3>
);
}, "3" );
>`src\App.tsx`
```tsx
import { UserInfoForm } from "./components/user-form";
function App() {
return (
<>
<UserInfoForm updatableUser={null} changeHandler={function (): void {
throw new Error("Function not implemented.");
} } userPostHandler={function (): void {
throw new Error("Function not implemented.");
} } resetUserHandler={function (): void {
throw new Error("Function not implemented.");
} } />
</>
);
}
export default App;
不再局限于更新用户数据,而是通过资源api和资源名称,更新通用数据。
src\components\include-updatable-resouce.tsx
import React, { useEffect, useState } from 'react'; import axios from 'axios';
const toCapital = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
type IncludeUpdatableResourceProps
export const includeUpdatableResouce = <T, P extends object>(
Component: React.ComponentType<P & IncludeUpdatableResourceProps
useEffect(() => {
(async () => {
const response = await axios.get(resourceUrl);
setData(response.data);
setUpdatableData(response.data);
})();
}, [resourceUrl]);
const changeHandler = (updates: Partial<T>) => {
setUpdatableData((prev) => (prev ? { ...prev, ...updates } : prev));
};
const dataPostHandler = async () => {
if (updatableData) {
const response = await axios.post(resourceUrl, {
[resourceName]: updatableData,
});
setData(response.data);
setUpdatableData(response.data);
}
};
const resetHandler = () => {
setUpdatableData(data);
};
const resourceProps = {
[resourceName]: updatableData,
[`onChange${toCapital(resourceName)}`]: changeHandler,
[`onSave${toCapital(resourceName)}`]: dataPostHandler,
[`onReset${toCapital(resourceName)}`]: resetHandler,
} as IncludeUpdatableResourceProps<T>;
return <Component {...props} {...resourceProps} />;
}; };
>`src\components\user-form.tsx`
```tsx
import React from 'react';
import { includeUpdatableResouce } from './include-updatable-resouce';
import { User } from './user-info';
type UserInfoFormProps = {
user: User | null;
onChangeUser: (updates: Partial<User>) => void;
onSaveUser: () => void;
onResetUser: () => void;
};
export const UserInfoForm = includeUpdatableResouce<User, UserInfoFormProps>(
({ user, onChangeUser, onSaveUser, onResetUser }: UserInfoFormProps) => {
const { name, age } = user || {};
return user ? (
<>
<label>
Name:
<input
value={name}
onChange={(e) => onChangeUser({ name: e.target.value })}
/>
</label>
<label>
Age:
<input
value={age}
onChange={(e) => onChangeUser({ age: Number(e.target.value) })}
/>
</label>
<button onClick={onResetUser}>Reset</button>
<button onClick={onSaveUser}>Save</button>
</>
) : (
<h3>Loading...</h3>
);
},
'/api/users/2',
'user'
);
src\App.tsx
import { UserInfoForm } from "./components/user-form";
function App() { return ( <> <UserInfoForm user={null} onChangeUser={function (): void { throw new Error("Function not implemented."); } } onSaveUser={function (): void { throw new Error("Function not implemented."); } } onResetUser={function (): void { throw new Error("Function not implemented."); } } /> </> ); }
export default App;
在本章中,我们将深入探讨自定义钩子这一强大的设计模式。
自定义钩子允许我们结合现有的 React 钩子,如 useState
和 useEffect
,创建可重用的钩子,以实现特定的功能。
那么,究竟什么是自定义钩子呢?
自定义钩子是我们通过结合 React 提供的基本钩子创建的钩子。与其在多个组件中重复相同的逻辑,不如将该逻辑封装到一个自定义钩子中。
这使我们能够将复杂的行为抽象为可重用的单元。
让我们考虑一个例子:我们希望组件从服务器获取用户信息。我们可以在组件内部加载用户信息,或者创建一个名为 useUsers
的自定义钩子来处理数据加载并封装相关功能。
我们稍后将探讨自定义钩子的实现,但这大致是自定义钩子的样子。
在组件中使用自定义钩子时,我们只需调用自定义钩子并将其返回值赋给一个变量。
需要注意的是,自定义钩子必须以 use
作为开头,这是 React 规定的要求。
这种命名约定与钩子内部的工作方式有关,但我们暂时不深入探讨这些细节。
就像高阶组件和容器组件一样,自定义钩子也具有类似的目的。
它们允许我们在多个组件之间共享复杂的行为。
通过在自定义钩子中封装特定功能,我们可以轻松地在多个组件中重用这些逻辑。
在 React 前端开发中,custom hooks
一般翻译为“自定义钩子”或“自定义 Hook”。
自定义钩子是开发者通过组合 React 提供的基本钩子(如 useState
、useEffect
等)来创建的钩子函数,用于封装和重用组件逻辑。与在每个组件中重复相同的逻辑相比,自定义钩子使得代码更加模块化和易于维护。
封装逻辑: 自定义钩子允许将组件中通用的状态逻辑提取到一个独立的函数中,便于在多个组件中重用。例如,一个自定义钩子可以处理数据获取、表单处理、订阅等逻辑。
提高代码复用性: 通过将通用逻辑封装在自定义钩子中,开发者可以避免在多个组件中重复相同的代码,从而提高代码的复用性和可维护性。
清晰的代码结构: 自定义钩子使组件代码更加简洁和清晰,因为它们将复杂的逻辑封装在一个单独的函数中,组件本身只负责调用这个钩子并使用其返回值。
以下是一个简单的自定义钩子示例,用于管理表单输入状态:
import { useState } from 'react';
// 自定义钩子:useFormInput
function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
const handleChange = (event) => {
setValue(event.target.value);
};
return {
value,
onChange: handleChange
};
}
// 组件示例
function MyFormComponent() {
const name = useFormInput('');
const email = useFormInput('');
const handleSubmit = (event) => {
event.preventDefault();
console.log('Name:', name.value);
console.log('Email:', email.value);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Name: </label>
<input type="text" {...name} />
</div>
<div>
<label>Email: </label>
<input type="email" {...email} />
</div>
<button type="submit">Submit</button>
</form>
);
}
自定义钩子是 React 中非常强大的工具,通过将通用逻辑封装成钩子函数,可以提高代码的复用性和可维护性,使得组件代码更加简洁和清晰。使用自定义钩子,可以使开发者更好地管理状态逻辑,并在不同组件之间共享复杂的行为。
src/
|-- components/
| |-- UserInfo.tsx
| |-- current-user.hook.ts
|-- types/
| |-- index.ts
|-- App.tsx
|-- main.tsx
src\types\index.ts
export type User = { name: string; age: number; country: string; books: string[]; };
>`src\components\current-user.hook.tsx`
```tsx
import { useEffect, useState } from 'react';
import axios from 'axios';
import { User } from '../types';
export const useCurrentUser = (): User | null => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
(async () => {
const response = await axios.get('/api/current-user');
setUser(response.data);
})();
}, []);
return user;
};
src\components\user-info.tsx
import React from 'react'; import { useCurrentUser } from './current-user.hook';
export const UserInfo = (): React.ReactElement => { const user = useCurrentUser();
if (!user) { return
const { name, age, country, books } = user;
return ( <>
<p>Age: {age} years</p>
<p>Country: {country}</p>
<h2>Books</h2>
<ul>
{books.map((book) => (
<li key={book}>{book}</li>
))}
</ul>
</>
); };
>`src\App.tsx`
```tsx
import { UserInfo } from "./components/user-info";
function App() {
return (
<>
<UserInfo />
</>
);
}
export default App;
src/
|-- components/
| |-- UserInfo.tsx
| |-- user.hook.ts
|-- types/
| |-- index.ts
|-- App.tsx
|-- main.tsx
src\types\index.ts
export type User = { name: string; age: number; country: string; books: string[]; };
src\components\user.hook.tsx
import { useEffect, useState } from 'react'; import axios from 'axios'; import { User } from '../types';
export const useUser = (userId: string): User | null => { const [user, setUser] = useState<User | null>(null);
useEffect(() => {
(async () => {
const response = await axios.get(/api/users/${userId}
);
setUser(response.data);
})();
}, [userId]);
return user; };
>`src\components\user-info.tsx`
```tsx
import React from 'react';
import { useUser } from './user.hook';
type UserInfoProps = {
userId: string;
};
export const UserInfo = ({ userId }: UserInfoProps): React.ReactElement => {
const user = useUser(userId);
const { name, age, country, books } = user || { name: '', age: 0, country: '', books: [] };
return user ? (
<>
<h2>{name}</h2>
<p>Age: {age} years</p>
<p>Country: {country}</p>
<h2>Books</h2>
<ul>
{books.map((book) => (
<li key={book}>{book}</li>
))}
</ul>
</>
) : (
<h1>Loading...</h1>
);
};
src\App.tsx
import { UserInfo } from "./components/user-info";
function App() { return ( <> <UserInfo userId={"1"}/> <UserInfo userId={"2"}/> <UserInfo userId={"3"}/> </> ); }
export default App;
## 4. 使用自定义钩子获取资源
```lua
src/
|-- components/
| |-- UserInfo.tsx
| |-- BookInfo.tsx
| |-- resource.hook.ts
|-- types/
| |-- index.ts
|-- App.tsx
|-- main.tsx
src\types\index.ts
export type User = { name: string; age: number; country: string; books: string[]; };
export type Book = { name: string; price: number; title: string; pages: number; };
>`src\components\resource.hook.tsx`
```tsx
import { useEffect, useState } from 'react';
import axios from 'axios';
export const useResource = <T,>(resourceUrl: string): T | null => {
const [resource, setResource] = useState<T | null>(null);
useEffect(() => {
(async () => {
const response = await axios.get(resourceUrl);
setResource(response.data);
})();
}, [resourceUrl]);
return resource;
};
src\components\user-info.tsx
import React from 'react'; import { useResource } from './resource.hook'; import { User } from '../types';
type UserInfoProps = { userId: string; };
export const UserInfo = ({ userId }: UserInfoProps): React.ReactElement => {
const user = useResource/api/users/${userId}
);
const { name, age, country, books } = user || { name: '', age: 0, country: '', books: [] };
return user ? ( <>
<p>Age: {age} years</p>
<p>Country: {country}</p>
<h2>Books</h2>
<ul>
{books.map((book) => (
<li key={book}>{book}</li>
))}
</ul>
</>
) : (
); };
>`src\components\book-info.tsx`
```tsx
import React from 'react';
import { useResource } from './resource.hook';
import { Book } from '../types';
type BookInfoProps = {
bookId: string;
};
export const BookInfo = ({ bookId }: BookInfoProps): React.ReactElement => {
const book = useResource<Book>(`/api/books/${bookId}`);
const { name, price, title, pages } = book || { name: '', price: 0, title: '', pages: 0 };
return book ? (
<>
<h3>{name}</h3>
<p>{price}</p>
<h3>Title: {title}</h3>
<p>Number of Pages: {pages}</p>
</>
) : (
<h1>Loading...</h1>
);
};
src\App.tsx
import { BookInfo } from "./components/book-info"; import { UserInfo } from "./components/user-info";
function App() { return ( <> <UserInfo userId={"1"}/> <BookInfo bookId={"2"}/> </> ); }
export default App;
![图片](https://github.com/WangShuXian6/blog/assets/30850497/c1f9831f-5e98-4b43-90c2-7b3b5a3d70be)
## 5. 更通用的自定义钩子 a More Generic Custom Hook
从多个数据源获取数据
src/ |-- components/ | |-- UserInfo.tsx | |-- data-source.hook.ts |-- types/ | |-- index.ts |-- utils/ | |-- data-utils.ts |-- App.tsx |-- main.tsx
>`src\types\index.ts`
```tsx
export type User = {
name: string;
age: number;
country: string;
books: string[];
};
src\components\data-source.hook.tsx
import { useEffect, useState } from 'react';
export const useDataSource = <T,>(getData: () => Promise
useEffect(() => { (async () => { const data = await getData(); setResource(data); })(); }, [getData]);
return resource; };
>`src\utils\data-utils.ts`
```tsx
import axios from 'axios';
export const fetchFromServer = <T,>(url: string) => async (): Promise<T> => {
const response = await axios.get(url);
return response.data;
};
export const getFromLocalStorage = (key: string) => (): string | null => {
return localStorage.getItem(key);
};
src\components\user-info.tsx
import React from 'react'; import { useDataSource } from './data-source.hook'; import { User } from '../types'; import { fetchFromServer, getFromLocalStorage } from '../utils/data-utils';
type UserInfoProps = { userId: string; };
export const UserInfo = ({ userId }: UserInfoProps): React.ReactElement => {
const user = useDataSource/api/users/${userId}
));
const loginAttempts = useDataSource<string | null>(getFromLocalStorage('logins'));
const { name, age, country, books } = user || { name: '', age: 0, country: '', books: [] };
return user ? ( <>
<p>Age: {age} years</p>
<p>Country: {country}</p>
<h2>Books</h2>
<ul>
{books.map((book) => (
<li key={book}>{book}</li>
))}
</ul>
<p>Login Attempts: {loginAttempts}</p>
</>
) : (
); };
>`src\App.tsx`
```tsx
import { UserInfo } from "./components/user-info";
function App() {
return (
<>
<UserInfo userId={"1"}/>
<UserInfo userId={"2"}/>
<UserInfo userId={"3"}/>
</>
);
}
export default App;
函数式编程是一种组织代码的方法,它强调最小化变异和状态变化,利用独立于外部数据的纯函数,并将函数视为一等公民。
虽然这个定义最初可能看起来有点晦涩,但如果你是函数式编程的新手,请不要慌张。我建议你做一些相关研究,因为这可以在你的开发者职业生涯中对你有很大帮助。
现在让我们讨论一下函数式编程在 React 中的一些应用。
一个常见的应用是在控制组件中,我们之前已经讨论过。控制组件允许我们通过传递必要的属性来管理组件状态,最小化组件对内部状态管理的依赖。
函数组件是 React 中函数式编程的另一个关键应用。与已经存在一段时间的类组件不同,函数组件体现了函数式编程范式,提供了一种简洁明了的定义组件的方法。
高阶组件(HOCs)是 React 中函数式编程的另一个例子,在本课程中我们已经探索过它们。HOCs 利用一等函数的概念,创建返回其他函数的可重用函数,提供强大的功能和组合能力。
接下来,我们将深入探讨另外三种设计模式,这些模式展示了函数式编程在 React 中的影响:递归组件、部分应用组件和组件组合。
递归组件依赖于递归来实现特定效果。它们可以非常强大,提供复杂问题的独特解决方案。请务必关注这一部分内容,它非常重要。
部分应用组件通过传递组件属性的一个子集来创建更具体的通用组件版本。这种技术允许代码重用和组件定制的灵活性。
最后但同样重要的是,组件组合涉及将多个组件组合成一个单一组件以实现所需效果。这种模式允许通过组合更简单的组件来创建更复杂的组件。
当我们探索这些设计模式时,我们看到函数式编程原则如何增强 React 应用程序的模块化、可重用性和可维护性。
递归模式或者更准确地说,递归组件是一个调用自身的组件,它从内部调用自己。
src\components\recursive.tsx
const isValidObj = (data: string | object) =>
typeof data === "object" && data !== null;
export const Recursive = ({ data }: { data: string | object }) => {
if (!isValidObj(data)) {
return <li>{data}</li>;
}
const pairs = Object.entries(data);
console.log(data);
return (
<>
{pairs.map(([key, value]) => {
return (
<li key={key}>
{key}:
<ul>
<Recursive data={value} />
</ul>
</li>
);
})}
</>
);
};
src\App.tsx
import { Recursive } from "./components/recursive";
import "./App.css";
const myNestedObject = {
key1: "value1",
key2: {
innerKey1: "innerValue1",
innerKey2: {
innerInnerKey1: "innerInnerValue1",
innerInnerKey2: "innerInnerValue2",
},
},
key3: "value3",
};
function App() {
return (
<>
<Recursive data={myNestedObject} />
</>
);
}
export default App;
src\components\composition.tsx
import React from "react";
type ButtonProps = {
size?: "small" | "large";
color?: string;
text: string;
};
export const Button: React.FC<ButtonProps> = ({
size,
color,
text,
...props
}) => {
return (
<button
style={{
fontSize: size === "large" ? "25px" : "16px",
backgroundColor: color,
}}
{...props}
>
{text}
</button>
);
};
export const SmallButton: React.FC<ButtonProps> = (props) => {
return <Button size="small" {...props} />;
};
export const SmallRedButton: React.FC<ButtonProps> = (props) => {
return <SmallButton size={"large"} color="crimson" {...props} />;
};
src\App.tsx
import "./App.css";
import { SmallButton, SmallRedButton } from "./components/composition";
function App() {
return (
<>
<SmallButton text={"I am small!"} />
<SmallRedButton text={"I am small and Red"} />
</>
);
}
export default App;
只是用组件的一部分
src\components\partial.tsx
import React from "react";
type ButtonProps = {
size?: "small" | "large";
color?: string;
text?: string;
};
// 定义高阶组件partial的类型
export function partial<T>(
Component: React.ComponentType<T>,
partialProps: Partial<T>
) {
return (props: T): JSX.Element => {
return <Component {...partialProps} {...props} />;
};
}
export const Button: React.FC<ButtonProps> = ({
size,
color,
text,
...props
}) => {
return (
<button
style={{
fontSize: size === "large" ? "25px" : "16px",
backgroundColor: color || "initial",
}}
{...props}
>
{text}
</button>
);
};
// 使用partial创建SmallButton
export const SmallButton = partial(Button, { size: "small" });
// 使用partial创建LargeRedButton
export const LargeRedButton = partial(Button, {
size: "large",
color: "crimson",
});
src\App.tsx
import { LargeRedButton, SmallButton } from "./components/partial";
function App() {
return (
<>
<SmallButton text={"I am small!"}/>
<LargeRedButton text="I am large and Red"/>
</>
);
}
export default App;
src\components\card.tsx
import React, { createContext, useContext } from "react";
// 定义Context的类型
interface ContextType {
test?: string;
}
// 创建带有初始值的Context
const Context = createContext<ContextType | null>(null);
type Props = {
children: React.ReactNode;
};
// Body组件
const Body: React.FC<Props> = ({ children }) => {
return <div style={{ padding: ".5rem" }}>{children}</div>;
};
// Header组件
const Header: React.FC<Props> = ({ children }) => {
const context = useContext(Context);
return (
<div
style={{
borderBottom: "1px solid black",
padding: ".5rem",
marginBottom: ".5rem",
}}
>
{children}
{/* 从context中安全地获取test值 */}
{context?.test}
</div>
);
};
// Footer组件
const Footer: React.FC<Props> = ({ children }) => {
return (
<div
style={{
borderTop: "1px solid black",
padding: ".5rem",
marginTop: ".5rem",
}}
>
{children}
</div>
);
};
type CardProps = {
test?: string;
children: React.ReactNode;
};
// Card组件
const Card: React.FC<CardProps> & {
Header: typeof Header;
Body: typeof Body;
Footer: typeof Footer;
} = ({ test, children }) => {
return (
<Context.Provider value={{ test }}>
<div style={{ border: "1px solid black" }}>{children}</div>
</Context.Provider>
);
};
Card.Header = Header;
Card.Body = Body;
Card.Footer = Footer;
export default Card;
src\App.tsx
import Card from "./components/card";
function App() {
return (
<Card test="Value">
<Card.Header>
<h1 style={{ margin: "0" }}>Header</h1>
</Card.Header>
<Card.Body>
He hid under the covers hoping that nobody would notice him there. It
really didn't make much sense since it would be obvious to anyone who
walked into the room there was someone hiding there, but he still held
out hope. He heard footsteps coming down the hall and stop in front in
front of the bedroom door. He heard the squeak of the door hinges and
someone opened the bedroom door. He held his breath waiting for whoever
was about to discover him, but they never did.
</Card.Body>
<Card.Footer>
<button>Ok</button>
<button>Cancel</button>
</Card.Footer>
</Card>
);
}
export default App;
pnpm i mitt -S
src\components\buttons.tsx
import { emitter } from "../App";
const Buttons = (props) => {
const onIncrementCounter = () => {
emitter.emit("increment");
};
const onDecrementCounter = () => {
emitter.emit("decrement");
};
return (
<div>
<button onClick={onIncrementCounter}>➕</button>
<button onClick={onDecrementCounter}>➖</button>
</div>
);
};
export default Buttons;
src\components\counter.tsx
import { useEffect, useState } from "react";
import { emitter } from "../App";
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const onIncrement = () => {
setCount((count) => count + 1);
};
const onDecrement = () => {
setCount((count) => count - 1);
};
emitter.on("increment", onIncrement);
emitter.on("decrement", onDecrement);
return () => {
emitter.off("increment", onIncrement);
emitter.off("decrement", onDecrement);
};
}, []);
return <div>#: {count}</div>;
};
export default Counter;
src\components\parent.tsx
import Buttons from "./buttons";
import Counter from "./counter";
const ParentComponent = (props) => {
return (
<>
<Buttons />
<Counter />
</>
);
};
export default ParentComponent;
src\App.tsx
import ParentComponent from "./components/parent";
import mitt from "mitt";
export const emitter = mitt();
function App() {
return (
<>
<ParentComponent />
</>
);
}
export default App;
独立的#alert-holder标签可以提高性能,防止非必要的Portals挂在同一标签上。
index.html
<!doctype html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="alert-holder"></div>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
src\App.tsx
import React, { useState, ReactNode } from "react";
import { createPortal } from "react-dom";
import "./App.css";
// App组件不需要额外的Props类型定义
function App() {
const [show, setShow] = useState<boolean>(false); // 明确useState中状态的类型是boolean
return (
<div style={{ position: "absolute", marginTop: "200px" }}>
<h1>Other Content</h1>
<button onClick={() => setShow(true)}>Show Message</button>
<Alert show={show} onClose={() => setShow(false)}>
A sample message to show.
<br />
Click it to close.
</Alert>
</div>
);
}
type AlertProps = {
children: ReactNode; // ReactNode允许任何可以渲染的内容,包括字符串、数字、React元素等
onClose: () => void; // onClose是一个不接受任何参数并且不返回任何内容的函数
show: boolean; // show是一个布尔值,控制Alert组件是否渲染
};
const Alert: React.FC<AlertProps> = ({ children, onClose, show }) => {
if (!show) return null; // 未显示时返回null
return createPortal(
<div className="alert" onClick={onClose}>
{children}
</div>,
document.querySelector("#alert-holder")! // 使用非空断言操作符(!)来表明element一定存在
);
};
export default App;
对自定义组件的引用
src\input.tsx
import React, { forwardRef, InputHTMLAttributes, Ref } from "react";
type CustomInputProps = InputHTMLAttributes<HTMLInputElement>;
const CustomInput = (props: CustomInputProps, ref: Ref<HTMLInputElement>) => {
return <input {...props} ref={ref} className="text-input" />;
};
export const Input = forwardRef<HTMLInputElement, CustomInputProps>(
CustomInput
);
src\App.tsx
import React, { useRef, FormEvent } from "react";
import "./App.css";
import { Input } from "./input";
function App() {
const inputRef = useRef<HTMLInputElement>(null);
function submitHandler(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
console.log(inputRef.current?.value);
}
return (
<form onSubmit={submitHandler}>
<Input ref={inputRef} />
<button type="submit" className="button">
Submit
</button>
</form>
);
}
export default App;
在任何React项目中都会遇到的一件事,那就是错误。 现在假设由于某种原因,比如在你的子组件内部出现了拼写错误或其他错误,会发生什么呢?
整个应用程序会变成空白。这是一个糟糕的体验。
在该组件崩溃或遇到错误时应该显示与该组件相关的内容。
错误边界只是一些类组件,它们内部有一个回退组件。也许这是你现在在React应用程序中使用类组件的唯一情况。
因此,当你用错误边界包裹你的应用程序或任何组件时,如果该子组件崩溃或遇到错误,作为父组件的错误边界将显示该回退组件。
错误边界是高度可重用的。
你可以简单地使用错误边界包裹购物车组件,并提供你想要的回退组件。
我想重复的另一个建议是,应该有一个包裹整个应用程序的错误边界。
因此,如果应用程序的任何部分出错,不要向用户显示一个空白页面,你可以显示一个带有文字的图片,告诉他们我们遇到了一些问题,请稍后再试或几分钟后再试等。
现在我们在错误时显示了一些内容,但错误的详细信息呢?
也许你想将它记录到某个服务中?
在React类组件中有一个非常有用的函数叫做componentDidCatch,它接收错误。
这些错误边界只捕捉由React渲染步骤引起的错误。
注意:如果你有一些与异步代码相关的错误,例如从API获取数据,或在useEffect内部,或使用setTimeout,由于它是异步的,这些错误边界不会被触发。
简单举例,在useEffect中抛出错误,由于获取数据的问题,但它没有被错误边界捕捉。
因为这些错误与React渲染步骤无关。
如果你想捕捉这样的错误,你可能需要使用catch方法。
对于异步代码的错误,错误边界不会被触发,你需要用适当的方法根据场景捕捉这些错误,只使用错误边界处理与React渲染步骤相关的错误。
这非常合理,因为这些错误不会导致应用程序完全变白。【异步错误出现之前可能已经渲染了一些界面】
错误边界只处理那些让应用程序完全变白的可怕体验,这时候我们需要错误边界帮助我们。
其他情况,你需要使用相关技术来处理。
src\error-boundry.tsx
import React, { ReactNode } from "react";
interface ErrorBoundaryProps {
fallback: ReactNode;
children: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
}
export class ErrorBoundary extends React.Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
state: ErrorBoundaryState = { hasError: false };
//错误状态接受
static getDerivedStateFromError(_: Error): ErrorBoundaryState {
return { hasError: true };
}
//捕获错误详细信息
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
console.log("Error: ", error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
//后备组件
return this.props.children;
}
}
src\child.tsx
import React, { useEffect } from "react";
export const Child: React.FC = () => {
useEffect(() => {
fetch("/")
.then(() => {
throw new Error("Fetch Error");
})
.catch((error) => {
console.warn("catch fech error", error);
});
throw new Error("UI Error");
}, []);
return <h1>Child Component</h1>;
};
src\App.tsx
import React from "react";
import "./App.css";
import { Child } from "./child";
import { ErrorBoundary } from "./error-boundry";
function App(): JSX.Element {
return (
<>
<h1>Parent Component</h1>
<ErrorBoundary fallback={<h1>Error in child</h1>}>
<Child />
</ErrorBoundary>
</>
);
}
export default App;
React中的键和状态保留问题
在这个非常基础的应用程序中,我们只有两个组件。 其中一个是计数器组件,它只有一个状态,就是计数值,并有两个按钮,一个用于增加,一个用于减少计数值,当然还有显示它们的功能。
如果你点击它们,我们可以看到衬衫和鞋子。我可以增加鞋子的计数值,或者像这样增加衬衫的计数值。但这里的问题是,每当我在它们之间切换时,两个计数器的状态都是相同的。这是因为<Counter />
的父级都是<></>
,完全相同。所以切换时不会重新渲染。虽然状态没有持久化,但是因为没有重新渲染,所以状态也不会变更。
src\counter.tsx
import { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount((c) => c - 1)}>-</button>
{count}
<button onClick={() => setCount((c) => c + 1)}>+</button>
</>
);
};
export default Counter;
src\App.tsx
import { useState } from "react";
import "./App.css";
import Counter from "./counter";
function App() {
const [changeShirts, setChangeShirts] = useState(false);
return (
<div>
{changeShirts ? (
<>
<span>Shirts counts: </span> <Counter />{" "}
</>
) : (
<>
<span>Shoes counts: </span> <Counter />{" "}
</>
)}
<br />
<button onClick={() => setChangeShirts((s) => !s)}>Switch</button>
</div>
);
}
export default App;
鞋子增加到6 切换为体恤依然为6【应该为0】
所以当React查看两个状态的父组件时,它会发现它们在树中的位置不同。
例如,对于这个计数器,我们可以给它一个div,同样在这里。而对于另一个计数器,我们给它一个section而不是div,因为如果你再次给它一个div,React会查看计数器的父组件,并假设它们还是相同的,不会重新渲染。
所以我们简单地更改父组件,使React理解这两个组件虽然相同,但它们在不同的位置,请重新渲染它们。
src\App.tsx
import { useState } from "react";
import "./App.css";
import Counter from "./counter";
function App() {
const [changeShirts, setChangeShirts] = useState(false);
return (
<div>
{changeShirts ? (
<div>
<span>Shirts counts: </span> <Counter />{" "}
</div>
) : (
<section>
<span>Shoes counts: </span> <Counter />{" "}
</section>
)}
<br />
<button onClick={() => setChangeShirts((s) => !s)}>Switch</button>
</div>
);
}
export default App;
现在,如果我增加衬衫的数量,然后切换,你可以看到状态被重置了。因为当你切换时,整个应用程序重新渲染,React DOM会查看这个状态。如果我们在这个状态中,它会获取这个div。当你切换到另一个状态时,它会查看父组件并期待div,但它看到的是section。它会说,“嘿,我们在不同的树中,所以让我们重新渲染计数器。”这就是为什么在这种情况下它会起作用。
通过使用键,你可以确定一个组件与其他实际相同的组件是独特的。通过在这里给一个键,比如说“shirt”,我们可以说这个键是“shirt”,而另一个键是“shoes”。
如果你保存它,现在增加衬衫的数量,切换,你会看到它现在重新渲染了。因为对于React来说,每次它查看计数器时,它会检查键。如果计数器的键是“shirt”,每当你切换时,它会说,“计数器还是那个计数器,但因为它有另一个键,我要重新渲染它。”所以这就是我们根据键区分组件的方法。
src\App.tsx
import { useState } from "react";
import "./App.css";
import Counter from "./counter";
function App() {
const [changeShirts, setChangeShirts] = useState(false);
return (
<div>
{changeShirts ? (
<>
<span>Shirts counts: </span> <Counter key="shirts" />{" "}
</>
) : (
<>
<span>Shoes counts: </span> <Counter key="shoes" />{" "}
</>
)}
<br />
<button onClick={() => setChangeShirts((s) => !s)}>Switch</button>
</div>
);
}
export default App;
切换后重新渲染,状态归0 再切换重新渲染,状态归0,因为没有持久化状态
如果你想知道为什么每次我们在计数器组件之间切换时,这个计数器会重置为零,那是因为我们没有在这里保留状态。每次React从头重新渲染一个组件时,它会将状态重置为默认值。如果你想保留状态,有很多方法可以做到,但现在我们不关注这个问题。
要记住的是,每当一个元素的键更改时,React会完全从头构建或重新渲染该组件或元素。这就是为什么我们在React中遍历或映射数组时必须使用键
当你使用像onClick、onFocus这样的事件时,它们会使用冒泡阶段,这意味着它们从最初被点击的元素开始触发,即这个div,然后是其内部的警告,然后是第一个父元素,即这个div。
现在,在某些情况下,如果你想使用捕获阶段触发事件,也就是说从上到下触发,你只需要在事件末尾添加“Capture”。它可以是onClick、onFocus任何事件,只需添加“Capture”以获取捕获阶段。
现在保存,如果点击“显示消息”并点击,你会看到首先触发的是外部div,然后是内部div。这意味着在捕获阶段,事件从上到下开始触发,首先是父元素,然后一直到达实际被点击的元素,即这个div。
所以,基于你希望事件的触发顺序,你可以使用默认的冒泡事件,或者使用捕获事件。
值得一提的是,你可以对任何元素使用捕获事件,这不仅限于创建门户的示例。我只是使用创建门户的示例来演示这一点。你可以为每个元素添加onClick Capture,它们的行为将与此完全相同。
index.html
<!doctype html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="alert-holder"></div>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
src\App.css
.alert {
position: absolute;
top: 10px;
left: 50%;
translate: -50%;
background-color: aquamarine;
color: black;
border-radius: 5px;
padding: 10px;
cursor: pointer;
}
由子组件开始触发事件,一直到父组件. 首先点击 Show Message 显示 Alert 组件。显示好父子组件。
点击 Alert 组件,开始冒泡,首先打印 Alert 组件内的日志 inner div
,
然后冒泡到Alert的父组件 ,打印 outer div
src\App.tsx
import { useState, ReactNode } from "react";
import { createPortal } from "react-dom";
import "./App.css";
interface AlertProps {
children: ReactNode;
onClose: () => void;
show: boolean;
}
function App() {
const [show, setShow] = useState(false);
return (
<div
onClick={() => console.log("outer div")}
style={{ position: "absolute", marginTop: "200px" }}
>
<h1>Other Content</h1>
<button onClick={() => setShow(true)}>Show Message</button>
<Alert show={show} onClose={() => setShow(false)}>
A sample message to show.
<br />
Click it to close.
</Alert>
</div>
);
}
const Alert: React.FC<AlertProps> = ({ children, onClose, show }) => {
if (!show) return null;
return createPortal(
<div
className="alert"
onClick={() => {
onClose();
console.log("inner div");
}}
>
{children}
</div>,
document.querySelector("#alert-holder") as Element
);
};
export default App;
由父组件开始触发事件,一直到子组件. 首先点击 Show Message 显示 Alert 组件。显示好父子组件。
点击 Alert 组件,开始捕获,首先打印 Alert 组件的父组件的日志 `outer div
,
然后到Alert组件 ,打印 inner div
src\App.tsx
import { useState, ReactNode } from "react";
import { createPortal } from "react-dom";
import "./App.css";
interface AlertProps {
children: ReactNode;
onClose: () => void;
show: boolean;
}
function App() {
const [show, setShow] = useState(false);
return (
<div
onClickCapture={() => console.log("outer div")}
style={{ position: "absolute", marginTop: "200px" }}
>
<h1>Other Content</h1>
<button onClick={() => setShow(true)}>Show Message</button>
<Alert show={show} onClose={() => setShow(false)}>
A sample message to show.
<br />
Click it to close.
</Alert>
</div>
);
}
const Alert: React.FC<AlertProps> = ({ children, onClose, show }) => {
if (!show) return null;
return createPortal(
<div
className="alert"
onClickCapture={() => {
onClose();
console.log("inner div");
}}
>
{children}
</div>,
document.querySelector("#alert-holder") as Element
);
};
export default App;
src\App.css
.tooltip {
position: absolute;
border: 2px solid black;
}
src\App.tsx
import { useState, useRef, useEffect, MutableRefObject } from "react";
import "./App.css";
function App() {
const [show, setShow] = useState<boolean>(false);
const [top, setTop] = useState<number>(0);
const buttonRef: MutableRefObject<HTMLButtonElement | null> = useRef(null);
useEffect(() => {
if (buttonRef.current === null || !show) {
setTop(0);
return;
}
const { bottom } = buttonRef.current.getBoundingClientRect();
setTop(bottom + 30);
}, [show]);
const now = performance.now();
while (now > performance.now() - 100) {
// 模拟延迟操作
}
return (
<>
<button ref={buttonRef} onClick={() => setShow((s) => !s)}>
Show
</button>
{show && (
<div
className="tooltip"
style={{
top: `${top}px`,
}}
>
Some text ...
</div>
)}
</>
);
}
export default App;
如果你点击这个按钮,你会看到这里显示了文本。这段文本有一个样式,其中有一个top属性,这个top实际上是基于上面的某些useEffect计算出来的。
在这个useEffect内部,每当我们在这里切换文本的显示或隐藏时,就会进行计算。首先,我们检查文本是否不存在或者实际上是隐藏的,如果是这样,我们会将它的top设置为零,或者如果它正在显示在屏幕上,我们会将它的top设置为相对于这个按钮底部的某个值加上30像素。所以这个文本显示的位置是根据按钮的位置来计算的。
当我点击显示,你会看到它溢出按钮,有点从上到下移动,有100毫秒滞后。
点击按钮时,因为show为true,首先渲染文本,但位置在按钮上,因为此时top为0.
100毫秒后,执行 useEffect,计算高度,文本位置下移到top 30处。
这个问题的原因是,在这个useEffect中,我们看到文本的默认位置实际上是零。即使你隐藏它,它也会将其设置回零。
让我们回顾一下这里发生了什么。假设我们刚刚刷新了页面,这是组件的第一次渲染。首先,它运行这段代码,跳过useEffect,继续运行其他代码并渲染所有内容,包括文本,但默认的top位置是零。当我点击显示按钮时,这个show状态从false变为true。结果,这个useEffect监听到了show状态的变化,并将其触发。但在这个useEffect检测到show状态变化时,它不会先计算再渲染,而是告诉整个组件先渲染,然后再做任何计算。因此,应用程序或组件将运行这段代码,跳过useEffect,继续运行其他代码并渲染按钮,首先渲染的位置是默认的零。然后,当渲染完成后,useEffect会进行计算,设置新的top位置,即底部加上30像素,然后我们有第二次渲染,显示新位置的文本。
正如你所见,普通的useEffect具有异步行为。每次触发时,先告诉整个组件渲染,然后执行其任务。如果任务导致组件重新渲染,在某些情况下,包括我们这里的例子,会导致这种滞后。
也许并不是每次都会遇到这个问题,但有时当你在组件中有些元素需要基于useEffect中的计算结果进行渲染时,可能会导致不良的用户体验。
为了解决这个问题,我们需要在渲染之前完成所有计算。所以我们希望在渲染之前计算底部位置,然后设置top位置。当top状态更新为新值加30像素后,useEffect会要求组件渲染,而不是先渲染再计算并更新状态然后再次渲染,这样就可以消除滞后。
为此,我们有一个叫做useLayoutEffect的钩子,它与useEffect完全相似。但我们说过,每当它触发时,首先执行其内部任务,然后如果需要渲染,组件将重新渲染。所以现在如果这样做,你会看到没有滞后。每次切换时,它会进行计算,然后我们看到组件根据位置渲染。
点击按钮,100毫秒后才渲染文本,虽然有延迟【卡顿】,但文本位置正确,没有闪烁。
src\App.tsx
import { useState, useRef, useEffect, MutableRefObject, useLayoutEffect } from "react";
import "./App.css";
function App() {
const [show, setShow] = useState<boolean>(false);
const [top, setTop] = useState<number>(0);
const buttonRef: MutableRefObject<HTMLButtonElement | null> = useRef(null);
useLayoutEffect(() => {
if (buttonRef.current === null || !show) {
setTop(0);
return;
}
const { bottom } = buttonRef.current.getBoundingClientRect();
setTop(bottom + 30);
}, [show]);
const now = performance.now();
while (now > performance.now() - 100) {
// 模拟延迟操作
}
return (
<>
<button ref={buttonRef} onClick={() => setShow((s) => !s)}>
Show
</button>
{show && (
<div
className="tooltip"
style={{
top: `${top}px`,
}}
>
Some text ...
</div>
)}
</>
);
}
export default App;
src\App.tsx
import Form from "./input";
function App() {
return (
<>
<Form />
<p>
It is a long established fact that a reader will be distracted by the
readable content of a page when looking at its layout.
</p>
<Form />
</>
);
}
export default App;
src\input.tsx
import { useState } from "react";
const Form = () => {
const [email, setEmail] = useState("");
return (
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
);
};
export default Form;
但如果我点击这个 email,你会看到它仍然聚焦到另一个输入框,而不是与之对应的这个输入框。正如你猜测的那样,这个问题是因为我们硬编码了 ID 和 HTML for 属性。在 HTML 中,我们不能有重复的 ID。每当我们有多个元素使用相同的 ID,它总是会选择第一个具有该 ID 的元素。
const id = String(Math.random());
import { useState } from "react";
const Form = () => {
const [email, setEmail] = useState("");
const id = String(Math.random());
return (
<div>
<label htmlFor={id}>Email</label>
<input
id={id}
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
);
};
export default Form;
问题: 我们点击它,可以聚焦,再点击另一个,也可以聚焦。如果你在检查看元素,你会看到这些 ID 是完全随机的。但这样做的问题在于,如果我们有服务器端渲染,这个页面在服务器上渲染并发送到客户端,每当客户端刷新时,它会有不同的 ID,这样就无法正常工作。因为服务器上生成的 ID 和客户端上新生成的 ID 不同,这会破坏应用程序的逻辑,导致混乱。所以,这在实际中并不可行。
useID 钩子仅用于为 HTML 元素分配唯一标识符,不要用它来生成随机字符串,这样不安全。useID 的唯一用途是为 HTML 元素分配唯一标识符。
import { useId, useState } from "react";
const Form = () => {
const [email, setEmail] = useState("");
const id = useId();
return (
<div>
<label htmlFor={`${id}-email`}>Email</label>
<input
id={`${id}-email`}
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<label htmlFor={`${id}-name`}>Name</label>
<input id={`${id}-name`} />
</div>
);
};
export default Form;
实现在首次渲染时将焦点设置到这个输入框上。所以每次刷新页面,输入框都会自动获得焦点
useEffect 会尝试运行这段代码,然而 inputRef.current
为 null,因为输入框尚未挂载。这样会导致一个错误,因为我们试图将焦点设置到一个不存在的元素上
src\App.tsx
import { useCallback, useEffect, useRef, useState, RefObject } from "react";
function App() {
const [show, setShow] = useState<boolean>(false);
const inputRef: React.MutableRefObject<HTMLInputElement | null> =
useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return (
<>
<button onClick={() => setShow((s) => !s)}>Switch</button>
{show && <input type="text" ref={inputRef} />}
</>
);
}
export default App;
这个问题的解决方案是,使用 useCallback 而不是 useRef。我们可以从顶部导入 useCallback。
当你传递一个 useCallback 给一个元素时,该元素会被传递给 useCallback 的第一个参数。因此,我们可以在 useCallback 中访问这个输入框,并对其执行任何操作。比如,我们可以设置输入框的焦点。因为 useCallback 类似于 useEffect,你需要传递一个依赖数组。让我们保存并切换按钮,你会看到输入框自动获得焦点。
刷新页面后,错误消失了。当点击切换按钮时,输入框被渲染并显示出来。useCallback 会在输入框渲染后立即执行并设置焦点。如果输入框被销毁,我们会遇到一个错误,因为 useCallback 会再次运行。因此,我们需要在回调函数中检查输入框是否为 null。如果为 null,直接返回,不执行任何操作。
现在,当你切换按钮时,输入框可以正确聚焦且没有错误。使用 useCallback 的想法是,当你想在元素渲染到实际 DOM 后执行某个操作时,可以传递一个 useCallback 给该元素,并在回调中执行操作。
需要注意的是,useCallback 传递的引用并不是实际的 useRef 钩子对象。你不能对其使用 inputRef.current
。如果你需要一个实际的引用,可以创建一个新的 useRef 并手动设置它的 current 属性。这样你可以同时拥有一个实际的引用和一个自定义的引用。
import { useCallback, useEffect, useRef, useState, RefObject } from "react";
function App() {
const [show, setShow] = useState<boolean>(false);
const realInputRef: React.MutableRefObject<HTMLInputElement | null> =
useRef(null);
const inputRef = useCallback((input: HTMLInputElement | null) => {
realInputRef.current = input;
if (input === null) return;
input.focus();
}, []);
return (
<>
<button onClick={() => setShow((s) => !s)}>Switch</button>
{show && <input type="text" ref={inputRef} />}
</>
);
}
export default App;
当 React 组件重新渲染时,所有在 JSX 中定义的回调函数(例如 ref
属性)都会再次运行。这意味着每次组件更新时,useCallback 钩子定义的回调函数也会重新执行。让我们看看这是如何与组件的挂载和卸载相关的。
在你的代码中,你定义了一个 useCallback
钩子来创建 inputRef
:
const inputRef = useCallback((input: HTMLInputElement | null) => {
realInputRef.current = input;
if (input === null) return;
input.focus();
}, []);
这个回调函数会在以下两种情况下运行:
input
元素第一次被添加到 DOM 中时,React 会调用这个回调函数,并将 input
元素作为参数传递。input
元素从 DOM 中移除时,React 会再次调用这个回调函数,并传递 null
作为参数。如果不做任何检查,当 input
元素被销毁时(即 inputRef
被设置为 null
),尝试访问或操作 input
会导致错误。这是因为此时 input
已经不存在。
在你的代码中,你通过以下方式防止了这种错误:
if (input === null) return;
这确保了当 input
被销毁时(即 input
为 null
),回调函数会立即返回,而不是尝试访问 input
的属性或方法,如 input.focus()
。
让我们再看看完整的代码,并理解其工作原理:
import { useCallback, useEffect, useRef, useState, RefObject } from "react";
function App() {
const [show, setShow] = useState<boolean>(false);
const realInputRef: React.MutableRefObject<HTMLInputElement | null> = useRef(null);
const inputRef = useCallback((input: HTMLInputElement | null) => {
realInputRef.current = input;
if (input === null) return; // 防止访问已销毁的元素
input.focus();
}, []);
return (
<>
<button onClick={() => setShow((s) => !s)}>Switch</button>
{show && <input type="text" ref={inputRef} />}
</>
);
}
export default App;
初始渲染:
show
状态为 false
,不渲染 input
元素。show
状态切换为 true
,input
元素被添加到 DOM 中,inputRef
回调函数执行,input
元素获得焦点。状态切换:
show
状态切换为 false
,input
元素从 DOM 中移除,inputRef
回调函数执行,input
参数为 null
。input
是否为 null
,如果是,则立即返回,避免尝试访问已销毁的元素。当 input
元素被销毁时,React 会再次调用 useCallback
创建的回调函数,并传递 null
作为参数。通过在回调函数中检查 input
是否为 null
,可以避免在 input
元素不存在时尝试访问其属性或方法,从而防止错误的发生。
src\input.tsx
import { forwardRef, ForwardedRef, InputHTMLAttributes } from "react";
interface CustomInputProps extends InputHTMLAttributes<HTMLInputElement> {}
const CustomInput = (
props: CustomInputProps,
ref: ForwardedRef<HTMLInputElement>
) => {
return <input {...props} ref={ref} className="text-input" />;
};
export const Input = forwardRef<HTMLInputElement, CustomInputProps>(
CustomInput
);
src\App.tsx
import { useRef, FormEvent } from "react";
import "./App.css";
import { Input } from "./input";
function App() {
const inputRef = useRef<HTMLInputElement>(null);
function submitHandler(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
if (inputRef.current) {
console.log(inputRef.current.value);
}
}
return (
<form onSubmit={submitHandler}>
<Input ref={inputRef} />
<button type="submit" className="button">
Submit
</button>
</form>
);
}
export default App;
在表单提交事件的上下文中,默认行为是提交表单并重新加载页面。使用 e.preventDefault() 可以防止这种默认行为,从而使你能够以编程方式处理表单提交,例如通过AJAX发送表单数据或在控制台中记录输入的值。
React 强调声明式编程,你应尽量避免命令式编程,但有时候必须使用。例如,当我们需要聚焦某个元素时,我们就需要使用 useRef。一个好的实践是,你可以限制传递给组件的 ref 的访问范围,比如你只希望它能够访问输入框的 focus 方法,而不是全部属性和方法。
为此,你可以使用一个名为 useImperativeHandle 的特定钩子。它可以帮助我们定义这个 ref 能够访问的具体内容。这个钩子接收两个参数,第一个是传递给组件的 ref,第二个是一个返回对象的函数,这个对象包含了我们希望暴露给外部的内容。
例如,我们可以定义一个简单的 sayHello 方法:
src\input.tsx
import {
forwardRef,
ForwardedRef,
InputHTMLAttributes,
useRef,
useImperativeHandle,
} from "react";
interface CustomInputProps extends InputHTMLAttributes<HTMLInputElement> {}
interface CustomInputHandle {
focus: () => void;
}
const CustomInput = (
props: CustomInputProps,
ref: ForwardedRef<CustomInputHandle>
) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus();
},
}));
return <input {...props} ref={inputRef} className="text-input" />;
};
export const Input = forwardRef<CustomInputHandle, CustomInputProps>(
CustomInput
);
import {
forwardRef,
ForwardedRef,
InputHTMLAttributes,
useRef,
useImperativeHandle,
useState,
ChangeEvent,
} from "react";
// 定义接口,扩展了 HTMLInputElement 的属性
interface CustomInputProps extends InputHTMLAttributes<HTMLInputElement> {}
// 定义接口,包含对外暴露的方法
interface CustomInputHandle {
focus: () => void;
value: string;
}
// 自定义输入组件,使用 forwardRef 转发 ref
const CustomInput = (
props: CustomInputProps,
ref: ForwardedRef<CustomInputHandle>
) => {
const inputRef = useRef<HTMLInputElement>(null);
const [value, setValue] = useState<string>("");
// 处理输入变化的函数
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
// 使用 useImperativeHandle 来定义暴露给父组件的属性和方法
useImperativeHandle(
ref,
() => ({
focus: () => {
inputRef.current?.focus();
},
value,
}),
//如果是空依赖,那么只会渲染一次,每次获取的value都是最初的值,不会更新
[value]
);
return (
<input
{...props}
ref={inputRef}
value={value}
onChange={handleChange}
className="text-input"
/>
);
};
// 使用 forwardRef 包装组件,并导出
export const Input = forwardRef<CustomInputHandle, CustomInputProps>(
CustomInput
);
它可以帮助我们将组件的渲染分为优先或即时更新和其他延迟或非即时更新。这些非即时更新将等待所有其他渲染完成后再获取状态并调用渲染。
一个输入框,每当你输入某些内容时,它将使用 set keyword 更新这个状态。 但是我们有一个小组件,我称之为 heavy component。 这意味着它是一个在渲染时需要一些时间的组件。
在这里故意设置了一些延迟,好吧,仅用于教学目的。所以每次你在输入框中按键时,它将在 100 毫秒内渲染。 所以问题是,当我开始输入时,你会看到输入框中的值会有延迟更新。 这会导致输入卡顿。
关键词的变化和显示在输入框内之间有一个延迟。这是因为我们每次在键盘上按键时都试图渲染一个重的、慢的组件,这个 heavy component。
src\components\heavy-component.tsx
const HeavyComponent = ({ keyword }: { keyword: string }) => {
const init = performance.now();
while (init > performance.now() - 100) {
//Slowing down the component on purpose.
}
return (
<>
<h2>I am a slow component</h2>
{keyword}
</>
);
};
export default HeavyComponent;
src\App.tsx
import { useState } from "react";
import HeavyComponent from "./components/heavy-component";
function App() {
const [keyword, setKeyword] = useState("");
return (
<>
<input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
<HeavyComponent keyword={keyword} />
</>
);
}
export default App;
memo 会监听传入的 props ,防止重复渲染。
react memo 本身并不能解决这个问题
src\components\heavy-component.tsx
import React from "react";
const Component = ({ keyword }: { keyword: string }) => {
const init = performance.now();
while (init > performance.now() - 100) {
//Slowing down the component on purpose.
}
return (
<>
<h2>I am a slow component</h2>
{keyword}
</>
);
};
export const HeavyComponent = React.memo(Component);
不再传递这个快速变化的关键词,当用户开始在输入框中输入时,
我们将传递它的一个延迟版本。称之为 deferred keyword.
延迟消失.
在幕后这个 heavy component 仍然很重。我们没有解决它。
但是我们已经在某种程度上断开了这个输入框的值与这里的 heavy component 的重渲染的联系,从而改善了我们应用程序的用户体验。
当我们使用这个 deferred value 时,究竟发生了什么?我们有一个称为渲染优先级的概念。当我们像这样说 set keyword 时,即将值传递给这个输入框,每当我们快速输入时,在几毫秒内,这个与 deferred value 无关的输入框的渲染将具有高优先级。所以渲染将在这个输入框上进行。但与这个延迟状态相关的组件,即我们这个 heavy component,将等到所有这些高优先级的渲染完成,然后才会渲染自己。所以每当我在这里快速输入时,比方说我们有十次重渲染。这个使用 hook 的 deferred keyword 将等到所有与这个快速输入字符串相关的渲染完成。然后它会抓取最终值,即这里的这个值,并将其传递给这里的 heavy component。所以我们会有多次这个组件的快速渲染,即这里的这个输入框。最后,当没有其他渲染时,这个 use deferred value 将抓取状态的值
在输入结束后,才会渲染重型组件。
src\App.tsx
import { useState, useDeferredValue } from "react";
import { HeavyComponent } from "./components/heavy-component";
function App() {
const [keyword, setKeyword] = useState("");
const deferredKeyword = useDeferredValue(keyword);
console.log('keyword:',keyword)
console.log('deferredKeyword',deferredKeyword)
return (
<>
<input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
<HeavyComponent keyword={deferredKeyword} />
</>
);
}
export default App;
你可以将其用于不同的示例,比如 suspense 例如,以及拥有一个回退组件,诸如此类
类似 useDeferredValue,但适用于不同的场景。
npm install styled-components
切换时,部分组件会执行耗时操作,这会导致页面冻结,无法点击其他按钮。 点击这个书评时,需要一些时间来渲染这些内容。你可以假设我们有一个评论列表,例如,我们要从服务器获取这些评论,并且它们很多,可能需要一些时间。如果你看看这个特定的组件,即评论组件,你会看到我们有一个包含300个成员的数组。非常简单,我们试图遍历它们并渲染一个非常简单的组件,每个评论有三毫秒的延迟。这就是为什么当你点击这个书评时,需要一些时间来显示这些内容。
因为当前当我们在这里时,当你点击这个书评,它将是一个即时更新。它会通过任何其他渲染或调用渲染,直到它完成渲染为止。在这里点击这个书评时,如果我试图点击任何其他按钮,在它获取或渲染自己时,我无法点击。
假设我点击这个书评,你会看到所有这些按钮都冻结了,因为整个应用程序都在等待这个渲染完成
src\components\cover.tsx
import { CoverContainer, Emoji } from "./styled-elements";
const Cover = () => {
return (
<CoverContainer>
<Emoji role="img" aria-label="Book Cover Emoji">
📚
</Emoji>
</CoverContainer>
);
};
export default Cover;
src\components\reviews.tsx
import React from "react";
import { ReviewsContainer } from "./styled-elements";
const Reviews = () => {
return (
<ReviewsContainer>
<ul>
{Array(300)
.fill("")
.map((_, i) => (
<Review key={i} index={i} />
))}
</ul>
</ReviewsContainer>
);
};
const Review = ({ index }: { index: number }) => {
const init = performance.now();
while (init > performance.now() - 3) {
// Fake slow down.
}
return <li>Review #{index}</li>;
};
export default Reviews;
src\components\writer.tsx
import { WriterContainer } from "./styled-elements";
const Writer = () => {
return <WriterContainer>Codelicks Academy</WriterContainer>;
};
export default Writer;
src\components\styled-elements.tsx
import styled from "styled-components";
export const StyledButton = styled.button`
background-color: #f1f1f1;
border: 1px solid #ccc;
padding: 10px 15px;
margin: 0 5px;
cursor: pointer;
border-radius: 5px;
font-size: 16px;
transition: background-color 0.3s ease;
&:hover {
background-color: #ddd;
}
&:focus {
outline: none;
border-color: #007bff;
}
`;
export const ReviewsContainer = styled.div`
ul {
list-style-type: none;
padding: 0;
}
li {
border-bottom: 1px solid #ccc;
padding: 10px;
font-size: 1.2em;
color: #333;
}
`;
export const WriterContainer = styled.div`
font-size: 1.5em;
font-weight: bold;
color: #333;
text-align: center;
margin: 20px 0;
`;
export const CoverContainer = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 3em;
`;
export const Emoji = styled.span`
font-size: 50px;
`;
src\App.tsx
import { useState } from "react";
import Cover from "./components/cover";
import Reviews from "./components/reviews";
import Writer from "./components/writer";
import { StyledButton } from "./components/styled-elements";
function App() {
const [section, setSection] = useState("Cover");
const sectionHandler = (sec: string) => {
setSection(sec);
};
return (
<>
<StyledButton onClick={() => sectionHandler("Cover")}>
Book Cover
</StyledButton>
<StyledButton onClick={() => sectionHandler("Reviews")}>
Book Reviews
</StyledButton>
<StyledButton onClick={() => sectionHandler("Writer")}>
Book's Writer
</StyledButton>
{section === "Cover" ? (
<Cover />
) : section === "Reviews" ? (
<Reviews />
) : (
<Writer />
)}
</>
);
}
export default App;
所以我想以某种方式使用一个 hook,这样当我在这个封面上并点击评论时,如果我改变主意,我可以立即转到这个作者,而不必等待它完成然后再回来。
解决这个问题的方法是,我想告诉 React 这个 set section 函数实际上是在更新 section 状态的值,在这种情况下是封面,它会显示封面,或者是评论时显示评论,等等。非常基本,非常简单。我想告诉 React,这个 set section 函数应该是可覆盖的。所以如果我说它将是评论,例如,我点击这个评论,但我改变主意,想点击这个封面,我应该能够覆盖之前的 set section,替换为封面。
useTransitio 返回一个数组或元组,包含两个值 isPending 和 start。这个是惯例,你可以给它起任何其他名字。
startTransition 需要一个工厂函数,你可以传递任何函数,特别是状态操作符,例如这里的 set section。它会将其标记为非即时更新,所以你可以在调用时简单地覆盖它。
通过在这里这样做,假设我点击书评,但我改变主意,可以转到作者,它不会冻结整个应用程序。因为现在感谢这个 useTransition hook,当我点击这个书评时调用的 set section,可以被覆盖并变成书作者。现在它被替换为另一个。
useTransition 和 useDeferred 的区别在于,如果你只是有一个快速变化的状态值,并且你想延迟读取那个值,你可以使用 useDeferred。但是如果你想延迟状态的更新,而不是读取状态,你可以使用 useTransition。
src\App.tsx
import { useState, useTransition } from "react";
import Cover from "./components/cover";
import Reviews from "./components/reviews";
import Writer from "./components/writer";
import { StyledButton } from "./components/styled-elements";
function App() {
const [section, setSection] = useState("Cover");
const sectionHandler = (sec: string) => {
setSection(sec);
};
return (
<>
<Button onClick={() => sectionHandler("Cover")}>Cover</Button>
<Button onClick={() => sectionHandler("Reviews")}>Book Reviews</Button>
<Button onClick={() => sectionHandler("Writer")}>Book's Writer</Button>
{section === "Cover" ? (
<Cover />
) : section === "Reviews" ? (
<Reviews />
) : (
<Writer />
)}
</>
);
}
const Button = ({ onClick, ...props }) => {
const [isPending, startTransition] = useTransition();
return (
<StyledButton
onClick={() => {
startTransition(() => {
onClick();
});
}}
{...props}
/>
);
};
export default App;
有 isPending,你可以利用它来提供更好的用户体验。例如,当 set section 正在等待时,你可以显示一段文字,比如“我正在加载”或“获取中”。如果你点击它,你会看到“我正在加载”,完成后它会消失。
import { useState, useTransition } from "react";
import Cover from "./components/cover";
import Reviews from "./components/reviews";
import Writer from "./components/writer";
import { StyledButton } from "./components/styled-elements";
function App() {
const [section, setSection] = useState("Cover");
const sectionHandler = (sec: string) => {
setSection(sec);
};
return (
<>
<Button onClick={() => sectionHandler("Cover")}>Cover</Button>
<Button onClick={() => sectionHandler("Reviews")}>Book Reviews</Button>
<Button onClick={() => sectionHandler("Writer")}>Book's Writer</Button>
{section === "Cover" ? (
<Cover />
) : section === "Reviews" ? (
<Reviews />
) : (
<Writer />
)}
</>
);
}
const Button = ({ onClick, ...props }) => {
const [isPending, startTransition] = useTransition();
return (
<>
<StyledButton
onClick={() => {
startTransition(() => {
onClick();
});
}}
{...props}
/>
{isPending && "Loading..."}
</>
);
};
export default App;
状态函数需要直接在 startTransition 内部调用。所以如果我们在这里有一个 setTimeout,并且在这个超时内部调用 set section,比如给它一个十毫秒的延迟。现在,如果你保存这个并点击书评,你会看到它再次冻结了整个应用程序,因为 startTransition 无法访问这个 set section。所以你的状态函数需要直接在 startTransition 内部调用。
const Button = ({ onClick, ...props }) => {
const [isPending, startTransition] = useTransition();
return (
<StyledButton
onClick={() => {
startTransition(() => {
setTimeout(() => {
onClick();
}, 0);
});
}}
{...props}
/>
);
};
需要使用 setTimeout 的话,你可以将整个 startTransition 函数包装在里面,这样回到应用程序并点击它,你会看到它不会冻结。
这些代码段会立即从上到下运行,不像状态,有点异步行为。我的意思是,如果你在这里做一个 console log,比如在 useTransition 或 startTransition 之前和之后这样做,我们在 startTransition 内部说 console log 比如在这里。如果我打开 inspect 并点击这个书作者,你会看到在 startTransition 之前,内部和之后,完全按照顺序立即执行,没有任何延迟。唯一的延迟发生在这个 set section 上,在需要时延迟更新状态。
pnpm i styled-components react-router -S
在 React Router 6 中使用 suspense 组件和功能
src\main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import Nav from "./components/nav";
import { mainRoute } from "./components/main";
import { booksRoute } from "./components/books";
import Club from "./components/club";
const router = createBrowserRouter([
{
element: <Nav />,
children: [
{ index: true, ...mainRoute },
{ path: "/books", ...booksRoute },
{ path: "/club", element: <Club /> },
],
},
]);
const root = ReactDOM.createRoot(document.getElementById("root")!);
root.render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
src\util\delay.ts
const delay = <T>(data: T, interval: number): Promise<T> => {
return new Promise((res) => {
setTimeout(() => {
res(data);
}, interval);
});
};
export default delay;
src\components\books.tsx
import { useLoaderData } from "react-router";
import delay from "../util/delay";
import { MainHeading } from "./styled-elements";
interface BooksLoaderData {
bookCount: number;
authors: string;
}
const Books = () => {
const { bookCount, authors } = useLoaderData() as BooksLoaderData;
return (
<div>
<MainHeading>Books</MainHeading>
<p>
<strong>Available Books: </strong>
{bookCount}
</p>
<p>
<strong>Authors:</strong> {authors}
</p>
</div>
);
};
async function loader() {
const bookCount = delay(10, 1000);
const authors = delay("Codelicks", 2000);
return {
bookCount: await bookCount,
authors: await authors,
};
}
export const booksRoute = { element: <Books />, loader };
src\components\club.tsx
import { MainHeading } from "./styled-elements";
const Club = () => {
return <MainHeading>Club</MainHeading>;
};
export default Club;
src\components\main.tsx
import { useLoaderData } from "react-router-dom";
import delay from "../util/delay";
import { MainContainer, MainHeading } from "./styled-elements";
const Main = () => {
const data = useLoaderData() as string;
return (
<MainContainer>
<MainHeading>Main - {data}</MainHeading>
</MainContainer>
);
};
async function loader() {
return await delay("Fetched Data", 1000);
}
export const mainRoute = { element: <Main />, loader };
src\components\nav.tsx
import { Outlet, useNavigation } from "react-router";
import { LoadingMessage, NavContainer, NavLink } from "./styled-elements";
const Nav = () => {
const { state } = useNavigation();
return (
<NavContainer>
<NavLink to={"/"}>Main</NavLink>
<NavLink to={"/books"}>Books</NavLink>
<NavLink to={"/club"}>Club</NavLink>
{state === "loading" && <LoadingMessage>Loading...</LoadingMessage>}
<Outlet />
</NavContainer>
);
};
export default Nav;
src\components\styled-elements.tsx
import { Link } from "react-router-dom";
import styled from "styled-components";
export const NavContainer = styled.div`
background-color: #333;
padding: 10px;
color: white;
text-align: center;
`;
export const NavLink = styled(Link)`
text-decoration: none;
color: white;
font-size: 18px;
margin-right: 10px;
&:hover {
text-decoration: underline;
}
`;
export const LoadingMessage = styled.div`
color: #ffcc00;
font-size: 16px;
margin-top: 10px;
`;
export const MainContainer = styled.div`
padding: 20px;
text-align: center;
`;
export const MainHeading = styled.h1`
color: #aff003;
font-size: 28px;
`;
当你访问 books
页面时,会显示加载文本,加载完成后显示实际数据。
为 index
设置了主路由,
为 club
设置了 element club
,
并使用扩展运算符将 loader
函数传递给主路由和 books
路由。
定义了一个延迟函数,它接收数据和一个以毫秒为单位的间隔,并返回一个在指定时间后解析的数据的 Promise。 这用于模拟从后端延迟获取数据,以展示我们将在示例应用程序中解决的问题。
books
组件与 main
组件非常相似,但会显示更多数据。
我们使用 useLoaderData
钩子获取 bookCount
和 authors
数据,并在加载完成后显示。
这些数据具有不同的延迟,如 bookCount
为一秒,authors
为两秒。
但是,当你点击 books
页面时,会在两秒后同时显示所有数据,而不是分别显示。
这是我们需要解决的第一个问题。
我们首先要解决的问题是,当你点击 main
页面时,会显示加载文本,加载完成后显示主页面和动态数据。
我们希望分离组件的延迟部分和静态部分,先显示静态部分,然后再显示延迟加载的数据。
为了解决这个问题,我们使用 React Router
提供的 defer
函数。我们在 loader
函数中返回一个 Promise,而不是实际数据。这样组件就会等待 Promise 解析。
const { promise } = useLoaderData();
与 return defer({ promise: delay("Fetched Data", 1000) });
中的 promise 名字必须相同.
Suspense 包裹的地方异步展示。
父级的 Main --
同步展示。
src\components\main.tsx
import { Await, defer, useLoaderData } from "react-router-dom";
import delay from "../util/delay";
import { MainContainer, MainHeading } from "./styled-elements";
import { Suspense } from "react";
const Main = () => {
const { promise } = useLoaderData();
return (
<MainContainer>
<MainHeading>
Main -
<Suspense fallback="Fetching...">
<Await resolve={promise}>
{(data) => {
return <strong>{data}</strong>;
}}
</Await>
</Suspense>
</MainHeading>
</MainContainer>
);
};
function loader() {
return defer({ promise: delay("Fetched Data", 1000) });
}
export const mainRoute = { element: <Main />, loader };
src\components\books.tsx
import { Await, defer, useLoaderData } from "react-router";
import delay from "../util/delay";
import { MainHeading } from "./styled-elements";
import { Suspense } from "react";
const Books = () => {
const { bookCountPromise, authorsPromise } = useLoaderData();
return (
<div>
<MainHeading>Books</MainHeading>
<p>
<strong>Available Books: </strong>
<Suspense fallback="Fetching...">
<Await resolve={bookCountPromise}>
{(data) => {
return <strong>{data}</strong>;
}}
</Await>
</Suspense>
</p>
<p>
<strong>Authors:</strong>
<Suspense fallback="Fetching...">
<Await resolve={authorsPromise}>
{(data) => {
return <strong>{data}</strong>;
}}
</Await>
</Suspense>
</p>
</div>
);
};
function loader() {
const bookCountPromise = delay(10, 1000);
const authorsPromise = delay("Codelicks", 2000);
return defer({
bookCountPromise,
authorsPromise,
});
}
export const booksRoute = { element: <Books />, loader };
src\components\books.tsx
import { Await, defer, useAsyncValue, useLoaderData } from "react-router";
import delay from "../util/delay";
import { MainHeading } from "./styled-elements";
import { Suspense } from "react";
const Books = () => {
const { bookCountPromise, authorsPromise } = useLoaderData();
return (
<div>
<MainHeading>Books</MainHeading>
<p>
<strong>Available Books: </strong>
<Suspense fallback="Fetching...">
<Await resolve={bookCountPromise}>
{(data) => {
return <strong>{data}</strong>;
}}
</Await>
</Suspense>
</p>
<p>
<strong>Authors:</strong>
<Suspense fallback="Fetching...">
<Await resolve={authorsPromise}>
<Authors />
</Await>
</Suspense>
</p>
</div>
);
};
const Authors=()=>{
const authors=useAsyncValue()
return <strong>{authors}</strong>;
}
function loader() {
const bookCountPromise = delay(10, 1000);
const authorsPromise = delay("Codelicks", 2000);
return defer({
bookCountPromise,
authorsPromise,
});
}
export const booksRoute = { element: <Books />, loader };
src\main.tsx
const Club = lazy(() => import("./components/club"));
import React, { lazy } from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import Nav from "./components/nav";
import { mainRoute } from "./components/main";
import { booksRoute } from "./components/books";
const Club = lazy(() => import("./components/club"));
const router = createBrowserRouter([
{
element: <Nav />,
children: [
{ index: true, ...mainRoute },
{ path: "/books", ...booksRoute },
{ path: "/club", element: <Club /> },
],
},
]);
const root = ReactDOM.createRoot(document.getElementById("root")!);
root.render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
点击club时才加载该组件
模拟延迟
src\main.tsx
import React, { lazy } from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import Nav from "./components/nav";
import { mainRoute } from "./components/main";
import { booksRoute } from "./components/books";
import delay from "./util/delay";
//const Club = lazy(() => import("./components/club"));
const Club = lazy(() => delay(import("./components/club"), 1000));
const router = createBrowserRouter([
{
element: <Nav />,
children: [
{ index: true, ...mainRoute },
{ path: "/books", ...booksRoute },
{ path: "/club", element: <Club /> },
],
},
]);
const root = ReactDOM.createRoot(document.getElementById("root")!);
root.render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
懒加载布局
src\components\nav.tsx
import { Outlet, useNavigation } from "react-router";
import { LoadingMessage, NavContainer, NavLink } from "./styled-elements";
import { Suspense } from "react";
const Nav = () => {
const { state } = useNavigation();
return (
<>
<NavContainer>
<NavLink to={"/"}>Main</NavLink>
<NavLink to={"/books"}>Books</NavLink>
<NavLink to={"/club"}>Club</NavLink>
{state === "loading" && <LoadingMessage>Loading...</LoadingMessage>}
</NavContainer>
<Suspense fallback={<NavContainer>Loading...</NavContainer>}>
<NavContainer>
<Outlet />
</NavContainer>
</Suspense>
</>
);
};
export default Nav;
src\components\main.tsx
import { Await, defer, useLoaderData } from "react-router-dom";
import delay from "../util/delay";
import { MainContainer, MainHeading } from "./styled-elements";
import { Suspense } from "react";
const Main = () => {
const { promise } = useLoaderData();
return (
<MainContainer>
<MainHeading>
Main -
<Suspense fallback="Fetching...">
<Await resolve={promise}>
{(data) => {
return <strong>{data}</strong>;
}}
</Await>
</Suspense>
</MainHeading>
</MainContainer>
);
};
export default Main;
src\components\main-loader.ts
import { defer } from "react-router";
import delay from "../util/delay";
export function loader() {
return defer({ promise: delay("Fetched Data", 1000) });
}
src\main.tsx
import React, { lazy } from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import Nav from "./components/nav";
import { booksRoute } from "./components/books";
import delay from "./util/delay";
import { loader } from "./components/main-loader";
//const Club = lazy(() => import("./components/club"));
const Club = lazy(() => delay(import("./components/club"), 1000));
const Main = lazy(() => delay(import("./components/main"), 1000));
const router = createBrowserRouter([
{
element: <Nav />,
children: [
{ index: true, loader: loader, element: <Main /> },
{ path: "/books", ...booksRoute },
{ path: "/club", element: <Club /> },
],
},
]);
const root = ReactDOM.createRoot(document.getElementById("root")!);
root.render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
React进阶开发 设计系统, 设计模式, 性能优化Advanced React Design System, Design Patterns, Performance
目录