WangShuXian6 / blog

FE-BLOG
https://wangshuxian6.github.io/blog/
MIT License
45 stars 10 forks source link

React进阶开发 设计系统, 设计模式, 性能优化Advanced React Design System, Design Patterns, Performance #193

Open WangShuXian6 opened 3 months ago

WangShuXian6 commented 3 months ago

React进阶开发 设计系统, 设计模式, 性能优化Advanced React Design System, Design Patterns, Performance

目录

  1. 介绍
  2. 设计模式布局组件
  3. 设计模式容器组件
  4. 设计模式受控和非受控组件
  5. 设计模式高阶组件
  6. 设计模式自定义钩子
  7. React中的函数式编程设计模式
  8. 更多设计模式
  9. 高级概念和钩子
  10. 代码清理技巧
  11. 可扩展项目架构
  12. API层和异步操作
  13. 使用React-Query的API层
  14. 状态管理模式
  15. 性能优化
  16. 设计系统核心概念
  17. 使用Figma构建组件的设计系统
  18. 在React中开发组件的设计系统
  19. 封装样式的设计系统
  20. 间距模式的设计系统
  21. 更复杂样式的设计系统模式
  22. 设计系统最终项目
  23. 高级Typescript介绍
  24. 高级Typescript钩子类型
  25. 高级Typescript类型Reducer
  26. 高级Typescript Context API类型
  27. 高级Typescript使用泛型
  28. 高级Typescript更多内容
  29. 高级Typescript组件模式
  30. 额外
  31. 附录 A - Typescript基础
  32. 旧版- 性能优化
WangShuXian6 commented 3 months ago

2. 设计模式布局组件 Design Patterns Layout Components

2. 介绍

图片 图片

3. 屏幕分割器 Screen Splitter

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;

图片

4. 屏幕分割器增强 Screen Splitter Enhancement

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;

图片

5. 列表 Lists

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 ( <>

{name}

  <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 { items: T[]; sourceName: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any ItemComponent: any ; }

export const RegularList = <T,>({ items, sourceName, ItemComponent }: RegularListProps): React.ReactElement => { return ( <> {items.map((item, i) => ( <ItemComponent key={i} {...{ [sourceName]: item }} /> ))} </> ); };


>`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 (

{name} / {price}

); };


>`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)
WangShuXian6 commented 3 months ago

3. 设计模式-容器组件 Design Patterns Container Components

1. 介绍 Introduction

图片

从某种意义上说,容器组件是负责数据加载和数据管理的React组件,它们为子组件处理这些任务。 这里显示的是容器组件包裹多个子组件的情况。

通常,如果你是一个初级或中级的React开发者,可能会让子组件自行加载数据并独立显示。

例如,你可能会使用Usestate和Useeffect钩子以及像Axios或Fetch这样的库来从服务器获取数据。

然而,当多个子组件需要共享相同的数据加载逻辑时,就会出现问题。

这时,容器组件就派上用场了。

它们通过将数据加载逻辑提取到一个专门的组件中来解决这个问题。

容器组件负责数据检索过程,并将数据自动传递给子组件。

很快我们将深入探讨容器组件如何实现这一点。

但在此之前,让我们先了解容器组件背后的核心概念,类似于布局组件,我们旨在让子组件不必了解它们所处的特定布局。

容器组件遵循类似的原则。

我们希望组件不知道其数据的来源或管理方式。

相反,它们只需接收props并显示相关内容,而无需了解底层的数据处理。

2. 服务器设置 Server Setup

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}`)
);

配置 react 到服务器的代理

普通React 项目

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 React 项目 [本项目使用该方式]

配置 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

3. 当前用户数据加载组件 3. Loader Component for CurrentUser Data

pnpm i axios -S

子组件 user-info

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

容器组件 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 })}</>;
};

App

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;

图片

4. 用户数据加载组件 Loader Component for User Data

之前的组件,只能获取当前用户的数据。 也许我们想根据 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}`)
);

通用用户信息容器组件 UserLoader

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;
      })}
    </>
  );
};

App

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;

图片

5. 资源数据加载组件 Loader Component for Resource Data

通用资源数据获取容器,通过动态api和动态子组件属性,为任意子组件获取数据

子组件 book-info

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>
  );
};

通用资源容器组件 resource-loader.

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;

图片

6. 数据源组件 DataSource Component

更通用的资源加载容器,无需关心是否有请求功能,无需关心数据源。只负责传递数据给子组件。

数据源容器组件,通过函数属性[替代内置的api请求]获取数据

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;
      })}
    </>
  );
};

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;
};

function App() {
  return (
    <>
      <DataSource
        getData={() => fetchData<User>("/api/users/1")}
        resourceName="user"
      >
        <UserInfo />
      </DataSource>
    </>
  );
}

export default App;

图片

7. 使用渲染属性模式的容器组件 Container Component with Render Props Pattern

注意,不应该在简单组件中使用 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)}</>;
};

user-info

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>
  );
};

App

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;

图片

8. 本地存储数据加载组件 Local Storage Data Loader Component

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;

图片 图片

WangShuXian6 commented 3 months ago

4. 设计模式-受控和非受控组件 Design Patterns Controlled and Uncontrolled Components

1. 介绍

在本章中,我们将探讨一个基本的 React 设计模式:受控和非受控组件。

这些模式在 React 中非常常见,因此理解它们的区别和使用场景是至关重要的。

非受控组件

让我们先了解一下 React 中的非受控组件。

非受控组件是指组件自身管理其内部状态,组件内的数据通常仅在特定事件发生时被访问。

一个常见的例子是非受控表单,表单输入的值只有在用户触发提交事件时才能被外部组件知道。

受控组件

另一方面,受控组件是指父组件负责管理状态,然后将状态传递给受控组件作为属性。

父组件处理状态并控制受控组件的行为。

这些是受控和非受控组件的基本定义。

现在让我们更仔细地看看这些概念在代码中的实现。

非受控组件

在非受控组件中,组件本身通常使用像 useState 这样的钩子来管理自己的状态。 图片

在这里提供的代码片段中,我们可以看到一个使用 useState 钩子的非受控组件。

传递给这个组件的唯一属性是 onSubmit,这是由父组件提供的一个函数,用于在提交事件发生时检索内部状态的值。

受控组件

在受控组件中,组件的状态不再由组件本身管理。

相反,状态是作为属性从父组件传递下来的。

在给出的示例中,你会注意到受控组件不再使用 useState 钩子。 图片

状态是作为属性从父组件接收的,并且相应地使用了额外的函数。

在本章中,我们将很快查看受控和非受控组件的具体示例。

现在一个常见的问题是,我们应该更倾向于使用哪种方式,受控组件还是非受控组件?

在大多数情况下,受控组件是首选。

这种偏好的原因有几个。

首先,受控组件更易用,也更易于测试。

使用受控组件,我们可以轻松设置所需状态的组件以进行测试。

这消除了手动操作组件和触发事件以检查其内部行为的需求。

2. 非受控组件 Uncontrolled Components

我们将创建一个非受控表单,所以我们称之为 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;

图片

3. 受控组件 Controlled Components

可以为组件添加额外功能,例如验证。

要创建的这个受控表单,它的基本区别在于,我们将使用像 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;

图片

4. 受控模态框 Controlled Modals

不再在内部更改它的状态(显示或隐藏),而是将其移到 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;

图片

5. 非受控流程 Uncontrolled Flows

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;

图片

6. 数据收集 Collecting Data

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;

7. 受控流程 Controlled Flows

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;
WangShuXian6 commented 3 months ago

5. 设计模式-高阶组件 Design Patterns HOCs

1. 介绍 Introduction

React设计模式:高阶组件。

高阶组件(简称HOC)是一些组件,它们不是直接返回JSX,而是返回另一个组件。

大多数React组件只是返回JSX,这些JSX代表将要渲染的DOM元素。

然而,通过高阶组件,我们引入了一个额外的层次,HOC不会直接返回JSX,而是返回另一个组件,这个组件再返回JSX。

为了简化这个概念,记住高阶组件本质上是返回组件的函数。

你可以把它们看作是组件工厂,当这些函数被调用时,它们会生成新的组件。

这种思维模型将帮助你掌握HOC的本质。

那么,为什么要创建高阶组件呢?

原因有几个。首先,HOC使我们能够在多个组件之间共享行为。

这类似于我们在容器组件中看到的,不同的组件被包装在同一个容器中,并表现出相似的行为。

高阶组件提供了一种实现类似功能的方法,用于共享相关的逻辑。

此外,高阶组件允许我们为现有组件添加额外功能。

如果我们遇到一个现有的组件,比如由其他人开发的遗留代码,HOC提供了一种方法,可以在不修改原始代码的情况下,为该组件增加新的功能和特性。

在本章的示例中,我们将更详细地探讨这些情况,展示高阶组件如何增强代码重用性和扩展组件功能。

2. 使用高阶组件检查属性 Checking Props with 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(UserInfo);

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}
      />
    );
  };
};

5. 使用高阶组件构建表单 Building Forms with HOC

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) => void; userPostHandler: () => void; resetUserHandler: () => void; };

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;

图片

6. 增强高阶组件模式 Enhancing HOC Pattern

不再局限于更新用户数据,而是通过资源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 = { [key: string]: T | ((updates: Partial) => void) | (() => void); };

export const includeUpdatableResouce = <T, P extends object>( Component: React.ComponentType<P & IncludeUpdatableResourceProps>, resourceUrl: string, resourceName: string ) => { return (props: P) => { const [data, setData] = useState<T | null>(null); const [updatableData, setUpdatableData] = useState<T | null>(null);

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;

WangShuXian6 commented 3 months ago

6. 设计模式自定义钩子 Design Patterns Custom hooks

1. 介绍 Introduction

在本章中,我们将深入探讨自定义钩子这一强大的设计模式。

自定义钩子允许我们结合现有的 React 钩子,如 useStateuseEffect,创建可重用的钩子,以实现特定的功能。

那么,究竟什么是自定义钩子呢?

自定义钩子是我们通过结合 React 提供的基本钩子创建的钩子。与其在多个组件中重复相同的逻辑,不如将该逻辑封装到一个自定义钩子中。

这使我们能够将复杂的行为抽象为可重用的单元。

让我们考虑一个例子:我们希望组件从服务器获取用户信息。我们可以在组件内部加载用户信息,或者创建一个名为 useUsers 的自定义钩子来处理数据加载并封装相关功能。 图片

我们稍后将探讨自定义钩子的实现,但这大致是自定义钩子的样子。

在组件中使用自定义钩子时,我们只需调用自定义钩子并将其返回值赋给一个变量。

需要注意的是,自定义钩子必须以 use 作为开头,这是 React 规定的要求。

这种命名约定与钩子内部的工作方式有关,但我们暂时不深入探讨这些细节。

就像高阶组件和容器组件一样,自定义钩子也具有类似的目的。

它们允许我们在多个组件之间共享复杂的行为。

通过在自定义钩子中封装特定功能,我们可以轻松地在多个组件中重用这些逻辑。


在 React 前端开发中,custom hooks 一般翻译为“自定义钩子”或“自定义 Hook”。

自定义钩子 (Custom Hooks)

解释

自定义钩子是开发者通过组合 React 提供的基本钩子(如 useStateuseEffect 等)来创建的钩子函数,用于封装和重用组件逻辑。与在每个组件中重复相同的逻辑相比,自定义钩子使得代码更加模块化和易于维护。

用法和好处

  1. 封装逻辑: 自定义钩子允许将组件中通用的状态逻辑提取到一个独立的函数中,便于在多个组件中重用。例如,一个自定义钩子可以处理数据获取、表单处理、订阅等逻辑。

  2. 提高代码复用性: 通过将通用逻辑封装在自定义钩子中,开发者可以避免在多个组件中重复相同的代码,从而提高代码的复用性和可维护性。

  3. 清晰的代码结构: 自定义钩子使组件代码更加简洁和清晰,因为它们将复杂的逻辑封装在一个单独的函数中,组件本身只负责调用这个钩子并使用其返回值。

示例

以下是一个简单的自定义钩子示例,用于管理表单输入状态:

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 中非常强大的工具,通过将通用逻辑封装成钩子函数,可以提高代码的复用性和可维护性,使得组件代码更加简洁和清晰。使用自定义钩子,可以使开发者更好地管理状态逻辑,并在不同组件之间共享复杂的行为。

2. 使用自定义钩子获取用户 Fetching a user with Custom Hook

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

Loading...

; }

const { name, age, country, books } = user;

return ( <>

{name}

  <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;

3. 使用自定义钩子获取多个用户 Fetching users with Custom Hook

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 ? ( <>

{name}

  <p>Age: {age} years</p>
  <p>Country: {country}</p>
  <h2>Books</h2>
  <ul>
    {books.map((book) => (
      <li key={book}>{book}</li>
    ))}
  </ul>
</>

) : (

Loading...

); };


>`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 | T): T | null => { const [resource, setResource] = useState<T | null>(null);

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(fetchFromServer(/api/users/${userId})); const loginAttempts = useDataSource<string | null>(getFromLocalStorage('logins')); const { name, age, country, books } = user || { name: '', age: 0, country: '', books: [] };

return user ? ( <>

{name}

  <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>
</>

) : (

Loading...

); };


>`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;
WangShuXian6 commented 3 months ago

7. React中的函数式编程设计模式 Design Patterns Functional Programming in React

1. 介绍

函数式编程是一种组织代码的方法,它强调最小化变异和状态变化,利用独立于外部数据的纯函数,并将函数视为一等公民。

虽然这个定义最初可能看起来有点晦涩,但如果你是函数式编程的新手,请不要慌张。我建议你做一些相关研究,因为这可以在你的开发者职业生涯中对你有很大帮助。

现在让我们讨论一下函数式编程在 React 中的一些应用。 图片

一个常见的应用是在控制组件中,我们之前已经讨论过。控制组件允许我们通过传递必要的属性来管理组件状态,最小化组件对内部状态管理的依赖。

函数组件是 React 中函数式编程的另一个关键应用。与已经存在一段时间的类组件不同,函数组件体现了函数式编程范式,提供了一种简洁明了的定义组件的方法。

高阶组件(HOCs)是 React 中函数式编程的另一个例子,在本课程中我们已经探索过它们。HOCs 利用一等函数的概念,创建返回其他函数的可重用函数,提供强大的功能和组合能力。

接下来,我们将深入探讨另外三种设计模式,这些模式展示了函数式编程在 React 中的影响:递归组件、部分应用组件和组件组合。

递归组件依赖于递归来实现特定效果。它们可以非常强大,提供复杂问题的独特解决方案。请务必关注这一部分内容,它非常重要。

部分应用组件通过传递组件属性的一个子集来创建更具体的通用组件版本。这种技术允许代码重用和组件定制的灵活性。

最后但同样重要的是,组件组合涉及将多个组件组合成一个单一组件以实现所需效果。这种模式允许通过组合更简单的组件来创建更复杂的组件。

当我们探索这些设计模式时,我们看到函数式编程原则如何增强 React 应用程序的模块化、可重用性和可维护性。

2. 递归组件 Recursive Components

递归模式或者更准确地说,递归组件是一个调用自身的组件,它从内部调用自己。

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;

图片

3. Compositions 组合组件[类似继承]

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;

图片

4. Partial Components 部分模式

只是用组件的一部分

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;

图片

WangShuXian6 commented 2 months ago

8. Design Patterns More Patterns

1. Compound Components 复合组件

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;

图片

2. Observer Pattern 观察员模式

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;

图片

WangShuXian6 commented 2 months ago

9. Advanced Concepts and Hooks 高级概念和钩子

1. React Portals React 传送门/门户

独立的#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;

2. Forwarding Refs 转发引用

对自定义组件的引用

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;

图片

3. Error Boundaries 错误边界

在任何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;

4. Keys Explained 键和状态

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。它会说,“嘿,我们在不同的树中,所以让我们重新渲染计数器。”这就是为什么在这种情况下它会起作用。 图片 图片 图片

使用 key

通过使用键,你可以确定一个组件与其他实际相同的组件是独特的。通过在这里给一个键,比如说“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中遍历或映射数组时必须使用键

5. Event Listeners 事件监听器

当你使用像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;
  }

冒泡 onClick

由子组件开始触发事件,一直到父组件. 首先点击 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;

图片

捕获 onClickCapture

由父组件开始触发事件,一直到子组件. 首先点击 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;

图片

6. useLayoutEffect

src\App.css

.tooltip {
    position: absolute;
    border: 2px solid black;
  }

普通的useEffect具有异步行为

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中的计算结果进行渲染时,可能会导致不良的用户体验。

useLayoutEffect 会先执行任务然后再渲染组件和元素

为了解决这个问题,我们需要在渲染之前完成所有计算。所以我们希望在渲染之前计算底部位置,然后设置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;

7. useId

图片

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 的元素。

初级解决方案 -随机 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

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;

8. useCallback As Ref

图片 实现在首次渲染时将焦点设置到这个输入框上。所以每次刷新页面,输入框都会自动获得焦点

错误示例

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 而不是 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;

要理解“如果输入框被销毁,我们会遇到一个错误,因为 useCallback 会再次运行”这句话,我们需要深入了解 React 中 useCallback 钩子的工作原理以及组件的生命周期。

组件生命周期与 useCallback

当 React 组件重新渲染时,所有在 JSX 中定义的回调函数(例如 ref 属性)都会再次运行。这意味着每次组件更新时,useCallback 钩子定义的回调函数也会重新执行。让我们看看这是如何与组件的挂载和卸载相关的。

useCallback 与 ref

在你的代码中,你定义了一个 useCallback 钩子来创建 inputRef

const inputRef = useCallback((input: HTMLInputElement | null) => {
  realInputRef.current = input;
  if (input === null) return;
  input.focus();
}, []);

这个回调函数会在以下两种情况下运行:

  1. 组件挂载时:当 input 元素第一次被添加到 DOM 中时,React 会调用这个回调函数,并将 input 元素作为参数传递。
  2. 组件卸载时:当 input 元素从 DOM 中移除时,React 会再次调用这个回调函数,并传递 null 作为参数。

销毁时的错误

如果不做任何检查,当 input 元素被销毁时(即 inputRef 被设置为 null),尝试访问或操作 input 会导致错误。这是因为此时 input 已经不存在。

在你的代码中,你通过以下方式防止了这种错误:

if (input === null) return;

这确保了当 input 被销毁时(即 inputnull),回调函数会立即返回,而不是尝试访问 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;

解释

  1. 初始渲染

    • show 状态为 false,不渲染 input 元素。
    • 按钮点击时,show 状态切换为 trueinput 元素被添加到 DOM 中,inputRef 回调函数执行,input 元素获得焦点。
  2. 状态切换

    • 再次点击按钮,show 状态切换为 falseinput 元素从 DOM 中移除,inputRef 回调函数执行,input 参数为 null
    • 回调函数检查 input 是否为 null,如果是,则立即返回,避免尝试访问已销毁的元素。

总结

input 元素被销毁时,React 会再次调用 useCallback 创建的回调函数,并传递 null 作为参数。通过在回调函数中检查 input 是否为 null,可以避免在 input 元素不存在时尝试访问其属性或方法,从而防止错误的发生。

9. useImperativeHandle

图片

原始 完全命令式

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发送表单数据或在控制台中记录输入的值。

useImperativeHandle 暴漏必要的命令式

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
);

10. useDeferredValue 延迟

它可以帮助我们将组件的渲染分为优先或即时更新和其他延迟或非即时更新。这些非即时更新将等待所有其他渲染完成后再获取状态并调用渲染。

消耗时间的重型组件

一个输入框,每当你输入某些内容时,它将使用 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;

React memo + useDeferredValue

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 例如,以及拥有一个回退组件,诸如此类

11. useTransition

类似 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;

useTransitio

所以我想以某种方式使用一个 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

状态函数需要直接在 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 上,在需要时延迟更新状态。

图片 图片

12. Async React Router 异步 React 路由器

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 钩子获取 bookCountauthors 数据,并在加载完成后显示。 这些数据具有不同的延迟,如 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 };

使用 defer 解决 bookCount,authors同时展示的问题

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 };

使用 useAsyncValue 获取异步数据,抽离渲染组件以优化

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;

分离loader和Suspense

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>
);
WangShuXian6 commented 2 months ago

10. Clean Code Tips 清洁代码技巧

1. Using Element Prop 使用元素属性

2. Optimizing Context API 优化上下文API

3. Less useEffects 减少使用useEffect

WangShuXian6 commented 2 months ago

11. Scalable Project Architecture 可扩展项目架构

1. General Architecture 一般架构

2. Route Components 路由组件

3. Encapsulating Components and Logics 封装组件和逻辑

WangShuXian6 commented 2 months ago

12. API Layer and Async Operations API层和异步操作

1.Building an API Layer 构建API层

2. API States API状态

3. Enhancing The API States 增强API状态

4. Avoiding Flickering Loaders 避免闪烁的加载器

5.Abstracting API States and Fetching Logic 抽象API状态和获取逻辑

6. Adding Request Abort Logic 添加请求中止逻辑

7. Logging Errors 记录错误

WangShuXian6 commented 2 months ago

13. API Layer with React-Query 使用React-Query的API层

1. Server Setup and a Quick Fix to withLogger Function 服务器设置和withLogger函数的快速修复

2. Fetching Data with React-Query 使用React-Query获取数据

3. Updating Data with React-Query 使用React-Query更新数据

4. Pagination with React-Query 使用React-Query分页

5. Infinite scroll with React-Query 使用React-Query无限滚动

6. Query Cancellation with React-Query 使用React-Query取消查询

WangShuXian6 commented 2 months ago

14. State Management Patterns 状态管理模式

1. Immutable updates with useImmer 使用useImmer进行不可变更新

2. Cleaner reducer with useImmerReducer 使用useImmerReducer清理reducer

WangShuXian6 commented 2 months ago

15. Performance Optimization 性能优化

1. Code-Splitting and Lazy-Loading 代码分割和懒加载

2. useCallback hook to preserve referential integrity 使用useCallback钩子保持引用完整性

3. Avoiding re-renders with useMemo 使用useMemo避免重新渲染

4. State Collocation 状态位置集中化

5. Preventing re-renders by lifting components up 提升组件以防止重新渲染

6. Throttling 节流

7. Debouncing 防抖

WangShuXian6 commented 2 months ago

16. Design System Core Concepts 设计系统核心概念

1. What is a design system 什么是设计系统

2. The importance of having a design system 拥有设计系统的重要性

3. Down sides of design systems 设计系统的缺点

4. Team Structure 团队结构

5. Audience of design systems 设计系统的受众

6. A real-life example 现实生活中的例子

7. The key concepts of design systems 设计系统的关键概念

8. A practical checklist 实用检查清单

9. Mistakes to avoid 避免的错误

WangShuXian6 commented 2 months ago

17. Design System Building Components Using Figma 使用Figma构建组件的设计系统

1. Section Overview 部分概述

2. Hands-on Color Palette in Figma 在Figma中实际操作颜色调色板

3. Hands-on Button Building Practice 按钮构建练习

4. Hands-on Designing a Modal 设计模态框练习

WangShuXian6 commented 2 months ago

18. Design System Developing Components in React 在React中开发组件的设计系统

1. Extensible Foundations 可扩展基础

2. Creating Button Component 创建按钮组件

3. Building a Modal 构建模态框

4. Reusability and Encapsulating Styles 重用和封装样式

WangShuXian6 commented 2 months ago

19. Design System Encapsulating Styles 封装样式的设计系统

1. Style Compositions 样式组合

2. Encapsulating Styles 封装样式

WangShuXian6 commented 2 months ago

20. Design System Patterns for Spacing 间距模式的设计系统

1. Overview 概述

2. Layers Pattern 层次模式

3. Split Pattern 分割模式

4. Column Pattern 列模式

5. Grid Pattern 网格模式

6. Inline-Bundle Pattern 内联捆绑模式

7. Inline Pattern 内联模式

WangShuXian6 commented 2 months ago

21. Design System Patterns for More Complex Styles 更复杂样式的设计系统模式

1. Overview 概述

2. Pad Pattern 填充模式

3. Center Pattern 居中模式

4. Media-Wrapper Pattern 媒体包装器模式

5. Cover Pattern 封面模式

6. Revisiting the Modal 重新审视模态框

WangShuXian6 commented 2 months ago

22. Design System Final Project 设计系统最终项目

1. Project Assignment 项目任务

2. Solution Building a Navbar with Menu and Header 解决方案:构建带菜单和标题的导航栏

3. Solution Building a Sidebar Menu 解决方案:构建侧边栏菜单

4. Solution Building the Form 解决方案:构建表单

5. Solution Finishing Buttons 解决方案:完成按钮

WangShuXian6 commented 2 months ago

23. Advanced Typescript Introduction 高级Typescript介绍

1. Requirements 要求

WangShuXian6 commented 2 months ago

24. Advanced Typescript Typing Hooks 高级Typescript钩子类型

1. useState 使用useState

2. State without initial state 无初始状态的状态

3. Passing States and Events Part1 传递状态和事件Part1

4. Passing States and Events Part2 传递状态和事件Part2

5. Refactoring Passing States and Events 重构传递状态和事件

6. Typing useRef 使用useRef

7. Typing Returned Values of a Custom Hook 自定义钩子返回值类型

8. Typing Complex States 复杂状态类型

9. Typing Complex States Part2 复杂状态类型Part2

10. Tuples with Custom Hooks 使用元组自定义钩子

WangShuXian6 commented 2 months ago

25. Advanced Typescript Typing Reducers 高级Typescript类型Reducer

1. Typing Reducers 类型Reducer

2. Passing Dispatch as a Prop Part1 作为属性传递的Dispatch Part1

3. Passing Dispatch as a Prop Part2 作为属性传递的Dispatch Part2

4. Template Literal Types 模板文字类型

5. Action and Reducer Types Action和Reducer类型

WangShuXian6 commented 2 months ago

26. Advanced Typescript Typing Context API 高级Typescript Context API类型

1. Context API with Types 使用类型的Context API

WangShuXian6 commented 2 months ago

27. Advanced Typescript Using Generics 高级Typescript使用泛型

1. Utility types 实用类型

2. Generics with Template Literals 带模板文字的泛型

3. More on Generics 更多泛型内容

4. Building a Context with Generics 使用泛型构建Context

5. Consuming a Custom Context 使用自定义Context

6. Building a Type Helper 构建类型助手

7. Another Type Helper 另一个类型助手

8. Generic Constrains 泛型约束

9. Typing a Hook with Generics 使用泛型类型钩子

10. Inferring Generic Types 推断泛型类型

11. Generic Components 泛型组件

12. Passing Types to Components 传递类型到组件

13. Reconsidering Generics 重新考虑泛型

WangShuXian6 commented 2 months ago

28. Advanced Typescript More on Typescript 高级Typescript更多内容

1. Types vs interfaces 类型 vs 接口

2. Function overloads 函数重载

WangShuXian6 commented 2 months ago

29. Advanced Typescript Component Patterns 高级Typescript组件模式

1. Higher Order components Part1 高阶组件 Part1

2. Higher Order components Part2 高阶组件 Part2

3. Render Props 渲染属性

4. Custom Hooks 自定义钩子

5. Limiting Prop Composition 限制属性组合

6. Requiring Prop Composition 需要属性组合

WangShuXian6 commented 2 months ago

30. Bonus 额外

1. Render Props 渲染属性

2. Wrapper Component 包装组件

3. Polymorphic Component 多态组件

WangShuXian6 commented 2 months ago

31. Appendix A - Typescript Basics 附录 A - Typescript基础

1. Typescript via Intellisense 通过Intellisense使用Typescript

2. Defining Type of Props 定义属性类型

3. Migrating From JS to TS Exercise 从JS迁移到TS练习

4. Defining Types for Children 定义子组件类型

5. Extending Props with Helpers 使用助手扩展属性

6. Props with Variant Types 带变体类型的属性

7. Requiring Props 需要的属性

8. Differentiating Props 区分属性

9. Empty Object as Type 空对象作为类型

10. Empty Object and Requiring Props 空对象和需要的属性

11. Understanding ReactNode 理解ReactNode

12. Linking Types 链接类型

13. Partial Autocomplete 部分自动完成

14. Extracting Types with as const 使用as const提取类型

15. Dynamic Props 动态属性

WangShuXian6 commented 2 months ago

32. ---LEGACY--- Performance Optimization 旧版- 性能优化

1. The demo project 演示项目

2. Getting up and running with the demo codes 使用示例代码启动和运行

3. Introduction to the React Profiler React Profiler介绍

4. Introduction to React Rendering React渲染介绍

5. The Virtual DOM 虚拟DOM

6. Preventing Wasted Renders in a Simple Component 在简单组件中防止浪费渲染

7. Preventing Wasted Renders in Functional Components 在函数组件中防止浪费渲染

8. Preventing Wasted Renders When Dealing With Complex Props 处理复杂属性时防止浪费渲染

9. Using Immutable Data in Order to Allow for Comparisons 使用不可变数据进行比较

10. Preventing Wasted Renders in Repeated Components 在重复组件中防止浪费渲染

11. Resources 资源

12. Catching Expensive Operations 捕捉昂贵操作

13. Reducing Bundle Sizes 减小包大小

14. Lazy Loading Components 懒加载组件

15. Resources 资源