react

目录在这里~

典型react项目目录结构

my-react-app/
├── public/
│   ├── index.html               # 主 HTML 文件
│   ├── favicon.ico              # 网站图标
│   └── manifest.json            # PWA 清单文件(如果使用 PWA)
├── src/
│   ├── assets/                   # 静态资源(如图片、字体等)
│   │   └── logo.png
│   ├── components/               # 可复用的 UI 组件
│   │   ├── Header.js
│   │   ├── Footer.js
│   │   └── Button.js
│   ├── hooks/                    # 自定义 Hook
│   │   └── useCustomHook.js
│   ├── pages/                    # 页面级组件
│   │   ├── HomePage.js
│   │   ├── AboutPage.js
│   │   └── NotFoundPage.js
│   ├── services/                 # API 请求和服务逻辑
│   │   └── apiService.js
│   ├── styles/                   # 样式文件
│   │   ├── global.css
│   │   └── theme.js
│   ├── utils/                    # 工具函数和辅助方法
│   │   └── helpers.js
│   ├── App.js                    # 根组件
│   ├── index.js                  # 应用入口点
│   ├── routes.js                 # 路由配置
│   ├── store/                    # Redux 状态管理(如果使用 Redux)
│   │   ├── actions/
│   │   │   └── actionTypes.js
│   │   ├── reducers/
│   │   │   └── rootReducer.js
│   │   ├── store.js
│   │   └── middleware.js
│   ├── constants/                # 常量定义
│   │   └── config.js
│   ├── context/                  # Context API(如果使用 Context)
│   │   └── AuthContext.js
│   └── tests/                    # 测试文件
│       └── App.test.js
├── .gitignore                    # Git 忽略文件
├── package.json                  # 包管理文件
├── README.md                     # 项目说明文档
└── tsconfig.json                 # TypeScript 配置文件(如果使用 TypeScript)

扩展目录结构

根据项目的具体需求,你可以进一步扩展和调整目录结构。以下是一些常见的扩展方式:

使用 TypeScript

如果你使用 TypeScript,可以在 src/ 目录下添加 types/ 目录来存放类型定义文件。

src/
├── types/
│   └── custom.d.ts

国际化支持

如果你的应用需要多语言支持,可以添加 locales/ 目录来存放翻译文件。

src/
├── locales/
│   ├── en-US.json
│   └── zh-CN.json

高阶组件和容器组件

为了更好地分离关注点,你可以将高阶组件(HOC)和容器组件单独存放在 hoc/ 和 containers/ 目录中。

src/
├── hoc/
│   └── withAuth.js
├── containers/
│   └── HomePageContainer.js

核心概念

组件化

函数组件

使用简单的函数定义,接收输入参数(Props),返回要渲染的元素。从 React 16.8 开始,函数组件可以通过 Hooks 使用状态和其他功能

  • 函数组件是无状态的,可以使用 React Hooks 来管理状态和副作用
import React, { useState } from 'react';

const MyComponent = () => {
    const [count, setCount] = useState(0); // useState Hook

    const increment = () => {
        setCount(count + 1);
    };

    return (
        <div>
            <h1>{count}</h1>
            <button onClick={increment}>增加</button>
        </div>
    );
};

export default MyComponent;

类组件

继承自 React.Component 类,可以通过 this.statethis.props 访问状态和属性。

  • 类组件通常用于需要管理状态或使用生命周期方法的场景
import React, { Component } from 'react';

class MyComponent extends Component {
    // 构造函数用于初始化 state
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
    }
    // 上面 constructor 这块的代码可以只写 state {count : 0}, 
    // 在类组件中,可以直接在类的主体中定义 state,而不必在构造函数中初始化。
    // 这种语法会自动将 state 绑定到实例上,因此不需要显式调用 super(props)。

    // 增加 count 的方法
    increment = () => {
        this.setState({ count: this.state.count + 1 });
    };

    render() {
        return (
            <div>
                <h1>{this.state.count}</h1>
                <button onClick={this.increment}>增加</button>
            </div>
        );
    }
}

export default MyComponent;

JSX

JSX 是一种扩展语法,允许在 JavaScript 中编写类似于 HTML 的标记语言。实际上,JSX 会被编译成普通的 JavaScript 函数调用,通常是 React.createElement()

// 基本表达
const element = <h1>Hello, world!</h1>;

//嵌入式表达
const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;

Props 和 State

props

  • 属性(Properties)是从父组件传递给子组件的数据。Props 是不可变的,子组件不能修改它接收到的 Props

    function ChildComponent(props) {
      return <div>{props.message}</div>;
    }
    
    function ParentComponent() {
      return <ChildComponent message="Hello from parent!" />;
    }
    

State

状态是组件内部用来存储数据的对象。它可以随时改变,当状态变化时,React 会自动重新渲染组件以反映最新的状态

// 类组件
class Counter extends React.Component {
  constructor(props) {
  super(props);
  this.state = { count: 0 };
}

increment = () => {
  this.setState({ count: this.state.count + 1 });
}

render() {
  return (
    <div>
      <p>Count: {this.state.count}</p>
      <button onClick={this.increment}>Increment</button>
    </div>
  )
 }
}

// 函数组件
import React, { useState } from 'react';

const MyComponent = () => {
    const [count, setCount] = useState(0); // useState Hook

    const increment = () => {
        setCount(count + 1);
    };

    return (
        <div>
            <h1>{count}</h1>
            <button onClick={increment}>增加</button>
        </div>
    );
};

export default MyComponent;

虚拟DOM

虚拟 DOM 是 React 内部使用的轻量级 DOM 树副本。当组件的状态或属性发生变化时,React 会先在虚拟 DOM 上进行更改,然后通过高效的算法计算出最小的更新操作,最后将这些操作应用到真实的 DOM 上,从而减少不必要的 DOM 操作,提高性能。

类名className

在 React 中,使用 className 属性来指定一个或多个 CSS 类,而不是使用 HTML 的 class 属性。这是因为 class 是 JavaScript 中的保留关键字,React 采用 className 来避免冲突

function MyComponent() {
  return <div className="my-class">Hello, World!</div>;
}

.my-class {
  color: blue;
  font-size: 20px;
}

动态类名

function MyComponent({ isActive }) {
  return (
    <div className={`my-class ${isActive ? 'active' : ''}`}>
      Hello, World!
    </div>
  );
}

jsx 中的 css

基本语法

在 React 中, 你可以使用 className 来指定一个 CSS 的 class。它与 HTML 的 class 属性的工作方式相同

  • 样式属性使用驼峰命名法(camelCase)。
  • 属性值通常为字符串或数字(需要附加单位时,使用字符串)。
    const MyComponent = () => {
        const style = {
            color: 'blue',
            backgroundColor: 'lightgray',
            padding: '10px',
            fontSize: '16px',
            border: '1px solid black'
        };
    
        return <div style={style}>这是一个内联样式的组件</div>;
    };
    

动态样式

内联样式的一个主要优势是可以很容易地根据组件的状态或属性动态更新样式。例如,可以根据 state 的值来改变样式

import React, { useState } from 'react';

const DynamicStyleComponent = () => {
    const [isActive, setIsActive] = useState(false);

    const style = {
        color: isActive ? 'white' : 'black',
        backgroundColor: isActive ? 'blue' : 'gray',
        padding: '10px',
        cursor: 'pointer'
    };

    return (
        <div 
            style={style} 
            onClick={() => setIsActive(!isActive)}
        >
            {isActive ? '激活状态' : '非激活状态'}
        </div>
    );
};

条件渲染

React 没有特殊的语法来编写条件语句,因此你使用的就是普通的 JavaScript 代码。例如使用if 语句根据条件引入 JSX

let content;
if (isLoggedIn) {
  content = <AdminPanel />;
} else {
  content = <LoginForm />;
}
return (
  <div>
    {content}
  </div>
);

渲染列表

你将依赖 JavaScript 的特性,例如 for 循环和 array 的 map() 函数来渲染组件列表

const products = [
  { title: 'Cabbage', id: 1 },
  { title: 'Garlic', id: 2 },
  { title: 'Apple', id: 3 },
];


const listItems = products.map(product =>
  <li key={product.id}>
    {product.title}
  </li>
);

return (
  <ul>{listItems}</ul>
);

响应事件

你可以通过在组件中声明事件处理函数来响应事件

  • 注意,onClick={handleClick} 的结尾没有小括号!不要调用事件处理函数:你只需 把函数传递给事件 即可。当用户点击按钮时 React 会调用你传递的事件处理函数
function MyButton() {
  function handleClick() {
    alert('You clicked me!');
  }

  return (
    <button onClick={handleClick}>
      Click me
    </button>
  );
}

生命周期

只有类组件才有生命周期函数。

React 组件有多个生命周期方法,它们在组件的不同阶段被调用。常用的生命周期方法包括:

  • componentDidMount: 组件挂载后调用
  • componentDidUpdate: 组件更新后调用
  • componentWillUnMount: 组件卸载前调用

生命周期对比表

类组件生命周期方法对应的 Hook
constructoruseState
getDerivedStateFromPropsuseState + useEffect
render返回 JSX
componentDidMountuseEffect (空数组依赖)
shouldComponentUpdateuseMemo 或 useCallback
getSnapshotBeforeUpdateuseLayoutEffect
componentDidUpdateuseEffect (依赖数组)
componentWillUnmountuseEffect (返回清理函数)

代码复用

高阶组件

React 高阶组件(Higher-Order Component, HOC)是一种用于复用组件逻辑的高级技术。HOC 本质上是一个函数,它接收一个组件并返回一个新的组件。通过这种方式,你可以将通用的逻辑提取出来并在多个组件之间共享。。HOCs 可以用于封装共享逻辑。

基本概念

高阶组件(HOC)是 React 中的一种设计模式,它遵循以下原则:

  • 输入和输出:HOC 是一个函数,它接受一个组件作为输入,并返回一个新的增强后的组件
  • 不修改原始组件:HOC 不会修改传入的组件,而是通过组合的方式创建一个新的组件。
  • 复用逻辑:HOC 主要用于在多个组件之间共享逻辑,例如权限控制、数据获取、样式注入等。

基本语法

// 定义一个高阶组件
function withEnhancement(WrappedComponent) {
  // 返回一个新的组件
  return function EnhancedComponent(props) {
    // 在这里可以添加额外的逻辑或属性
    return <WrappedComponent {...props} newProp="someValue" />;
  };
}

// 使用高阶组件包装一个普通组件
const EnhancedMyComponent = withEnhancement(MyComponent);

注意事项

  • 命名约定:通常情况下,高阶组件的名称以 with 开头,以便明确其用途。例如,withCounter, withAuthorization, withDataFetching 等。

  • 传递 props:确保高阶组件正确地将所有 props 传递给被包装的组件。这可以通过扩展运算符 {...props} 来实现。

  • 静态方法丢失:当使用高阶组件包装组件时,原始组件的静态方法会被丢失。你可以通过手动转发静态方法来解决这个问题。

    // 手动转发静态方法
    WrappedComponent.staticMethod = someFunction;
    
  • Ref 转发:如果你需要访问被包装组件的 DOM 节点,可以使用 React.forwardRef 来转发 ref

    function withCounter(WrappedComponent) {
      return React.forwardRef((props, ref) => {
        const [count, setCount] = useState(0);
    
        const increment = () => {
          setCount(count + 1);
        };
    
        return (
          <WrappedComponent
            ref={ref}
            count={count}
            increment={increment}
            {...props}
          />
        );
      });
    }
    

使用场景

  • 权限控制:根据用户角色控制组件的可见性
  • 数据获取:从 API 获取数据并在多个组件中展示。
  • 样式注入:为组件添加特定的样式或类名。
  • 日志记录:在组件生命周期的不同阶段记录日志。

Render Props

Render Props 是一种在 React 组件之间共享代码的技术,通过将函数作为 props 传递来实现

class MouseTracker extends React.Component {
  // ...

  render() {
    return this.props.render(this.state.mousePosition);
  }
}

function App() {
  return (
    <MouseTracker render={mousePosition => (
      <h1>The mouse position is {mousePosition.x}, {mousePosition.y}</h1>
    )} />
  )
}

hooks

以 use 开头的函数被称为 Hook。useState 是 React 提供的一个内置 Hook。

Hook 比普通函数更为严格。你只能在你的组件(或其他 Hook )的 顶层 调用Hook。如果你想在一个条件或循环中使用 useState ,请提取一个新的组件并在组件内部使用它。

使用 Hooks 的规则

  • 只能在函数组件或自定义 Hooks 中调用。 不可以在 普通 JavaScript 函数类组件 或者 条件语句 中调用。
  • 调用顺序必须一致。 React 会根据调用的顺序来追踪每个 Hook 的状态

常用的 Hook

useState

用于在函数组件中添加状态(state)管理。


import React, { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
         Click me
      </button>
    </div>
  )
}

useEffect

用于在函数组件中执行副作用操作,比如订阅数据、设置定时器、手动操作 DOM 等。 替代 componentDidMount, componentDidUpdate, 和 componentWillUnmount。

基本用法

* 第一个参数是一个回调函数,第二个参数是一个依赖数组。 * 当依赖项变化时,回调函数会执行;如果依赖数组为空,则只在组件挂载和卸载时执行。 * **返回值(可选)**:一个清理函数,用于在组件卸载或下次副作用执行前进行清理工作。

useEffect(() => {
  // 副作用逻辑
  return () => {
    // 清理逻辑(可选)
  };
}, [依赖项列表]); // 依赖项数组
执行时机
  • 首次渲染时:默认情况下,useEffect 在组件首次渲染完成后执行。
  • 依赖项变化时:如果提供了依赖项数组,当数组中的任何依赖项发生变化时,useEffect 会重新执行。
  • 组件卸载时:如果 useEffect 返回了一个清理函数,那么该清理函数会在组件卸载时执行,或者在下次副作用执行前执行。
应用场景
  • 数据获取:在组件挂载后从服务器获取数据。
  • 设置订阅:订阅外部数据源(如 WebSocket、事件等),并在组件卸载时取消订阅。
  • 手动 DOM 操作:在特定情况下手动操作 DOM。
  • 监听窗口大小变化:监听窗口大小的变化,并根据新的尺寸更新状态。
  • 设置定时器:设置定时器,并在组件卸载时清除定时器。
  • 与第三方库集成:将 React 组件与第三方库集成,并在组件卸载时清理资源。
  • 表单验证:监听表单输入的变化,并在必要时进行表单验证。
  • 文档标题更新:动态更新文档的标题,以反映当前页面的状态。
  • 动画效果:触发和管理动画效果,例如在组件挂载时启动动画,在卸载时停止动画。
useEffect 的依赖项数组

依赖项数组是 useEffect 的一个重要特性,它决定了副作用何时重新执行。

  • 空依赖项数组 ([]):表示只在组件挂载和卸载时执行副作用。
  • 包含具体依赖项的数组:当数组中的任何一个依赖项发生变化时,副作用会重新执行。
  • 不提供依赖项数组:默认情况下,副作用会在每次渲染后执行。
依赖项副作用函数执行时机
没有依赖项组件初始渣染 + 组件更新时执行
空数组依赖只在初始染时执行一次
添加特定依赖项组件初始染+特性依赖项变化时执行
清理函数

清理函数允许你在组件卸载或下次副作用执行前进行一些清理工作,比如取消订阅、移除事件监听器等

对比 useEffect 和 Vue 的组合式 API
功能需求React useEffectVue Composition API
组件挂载时执行副作用useEffect(() => { /* 副作用 */ }, [])onMounted(() => { /* 副作用 */ })
组件卸载时清理资源useEffect(() => { return () => { /* 清理 */ }; }, [])onUnmounted(() => { /* 清理 */ })
监听状态变化并执行副作用useEffect(() => { /* 副作用 */ }, [依赖项])watch(依赖项, (newValue, oldValue) => { /* 副作用 */ })
    // useEffect 没有提供依赖项数组,因此它会在每次组件渲染后执行
    import { useState, useEffect } from 'react';

    function Example() {
      const [count, setCount] = useState(0);

      useEffect(() => {
        console.log(`You clicked ${count} times`);
      });

      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
        </div>
      );
    }

    // useEffect 只有在 count 发生变化时才会重新执行。
    import { useState, useEffect } from 'react';

    function Example() {
      const [count, setCount] = useState(0);

      useEffect(() => {
        console.log(`You clicked ${count} times`);
      }, [count]); // 只有当 count 发生变化时才会重新执行

      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
        </div>
      );
    }

    // 带清理函数
    import { useState, useEffect } from 'react';

    function Example() {
      const [width, setWidth] = useState(window.innerWidth);

      useEffect(() => {
        const handleResize = () => setWidth(window.innerWidth);
        
        window.addEventListener('resize', handleResize);

        // 清理函数
        return () => {
          window.removeEventListener('resize', handleResize);
        };
      }, []); // 空依赖数组表示只在组件挂载和卸载时执行

      return (
        <div>
          <p>Window width: {width}px</p>
        </div>
      );
    }

useRef

用于函数组件中创建可变的引用。

 /*
• useRef: 创建一个 ref,用于存储对 DOM 元素的引用。
• ref 属性: 将 ref 赋值给要获取的元素,例如 &lt;input&gt;。
• 访问 DOM 元素: 通过 inputRef.current 来访问 DOM 元素
*/
import React, { useRef } from 'react';
function FocusInput() {
  const inputRef = useRef(null);	

  const handleFocus = () => {
    inputRef.current.focus(); // 获取到的 DOM 元素调用 focus 方法
  };

  return (
    <div>
      <input type="text" ref={inputRef} />
      <button onClick={handleFocus}>Focus the input</button>
    </div>
  );
}

export default FocusInput;

// 类组件中通过React.createRef() 创建 refs

import React, { Component } from 'react';

class FocusInput extends Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }

  handleFocus = () => {
    this.inputRef.current.focus();
  };

  render() {
    return (
      <div>
        <input type="text" ref={this.inputRef} />
        <button onClick={this.handleFocus}>Focus the input</button>
      </div>
    );
  }
}

export default FocusInput;

useContext

用于在函数组件中访问上下文(Context)。上下文提供了一种无需显式传递 props 就可以在组件树中传递数据的方式。这对于需要将某些值(如主题、用户信息等)传递给多个嵌套层级的子组件时特别有用

上下文的基本概念

在 React 中,上下文(Context)允许你通过组件树传递数据,而不需要手动在每一层通过 props 传递。这可以大大简化深层嵌套组件之间的数据共享问题。

创建和使用上下文

要使用 useContext,你需要首先创建一个上下文对象,并通过 Provider 组件将其值传递给子组件树中的任何组件。

  • 你可以使用 React.createContext() 来创建一个新的上下文对象。

    import React from 'react';
    
    // 创建一个上下文对象
    const MyContext = React.createContext();
    
  • 提供上下文值

    使用 MyContext.Provider 组件包裹需要访问上下文值的子组件,并通过 value 属性传递数据。

    import React, { useState } from 'react';
    import { MyContext } from './App'; // 假设上下文对象在此文件中定义或导入
    
    function App() {
      const [theme, setTheme] = useState('light');
    
      return (
        <MyContext.Provider value={{ theme, setTheme }}>
          <Toolbar />
        </MyContext.Provider>
      );
    }
    
    function Toolbar() {
      return (
        <div>
          <ThemedButton />
        </div>
      );
    }
    
  • 在函数组件中使用 useContext

    在需要访问上下文值的函数组件中,你可以使用 useContext Hook 来读取上下文值。

    import React, { useContext } from 'react';
    import { MyContext } from './App'; // 假设上下文对象在此文件中定义或导入
    
    function ThemedButton() {
      const { theme, setTheme } = useContext(MyContext);
    
      return (
        <button
          onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
          style={{ background: theme === 'light' ? '#fff' : '#000', color: theme === 'light' ? '#000' : '#fff' }}
        >
          Toggle Theme
        </button>
      );
    }
    

useCallback

用于优化性能,特别是在处理函数引用时。它确保回调函数的引用在依赖项没有变化的情况下保持不变。这对于避免不必要的重新渲染和优化组件性能特别有用

基本用法

useCallback 的主要作用是缓存一个回调函数,并仅在指定的依赖项发生变化时重新创建该函数。这有助于防止子组件因父组件重新渲染而无谓地重新渲染

  • 第一个参数:你希望记忆的回调函数。

  • 第二个参数:一个依赖项数组,类似于 `useEffect`。只有当数组中的某个依赖项发生变化时,`useCallback` 才会返回一个新的函数实例;否则,它会返回之前缓存的函数实例。

// 基本用法
const memoizedCallback = useCallback(
  () => {
    // 回调函数的实现
  },
  [依赖项列表] // 依赖项数组
);

useLayoutEffect

类似于 useEffect,但它在所有的 DOM 变更之后同步执行。这意味着它会在浏览器绘制之前完成,适合用于需要在浏览器渲染之前读取或修改 DOM 的场景。这与 useEffect 不同,后者是在浏览器绘制之后异步执行的

useLayoutEffect 非常强大,但由于它是同步执行的,可能会阻塞浏览器的渲染过程,因此只有在确实需要同步执行某些操作时才使用它。通常情况下,推荐首先尝试使用 useEffect,因为它不会阻塞浏览器的渲染

useLayoutEffectuseEffect 的区别
  • useEffect:在浏览器绘制之后异步执行,适合处理大多数副作用(如数据获取、订阅、手动 DOM 操作等)。
  • useLayoutEffect:在所有 DOM 变更之后同步执行,适合处理需要在浏览器绘制之前进行的操作,例如测量 DOM 元素的尺寸或位置。
使用场景

在某些特定情况下是必须的,尤其是在你需要在浏览器重新渲染之前同步执行某些操作时

  • **测量 DOM 元素尺寸或位置:**如果你需要在组件挂载后立即测量某个 DOM 元素的尺寸或位置,并且希望避免视觉上的闪烁或跳动,那么应该使用 useLayoutEffect
  • **同步 DOM 操作:**当你需要在浏览器重新渲染之前进行某些同步的 DOM 操作(例如调整样式、添加或移除类等),以确保用户不会看到中间状态的变化时,应该使用 useLayoutEffect
  • **避免视觉闪烁或跳动:**假设你有一个场景,其中你需要在用户交互后立即更新 DOM,并且不希望看到任何视觉上的闪烁。你可以使用 useLayoutEffect 来确保在浏览器重新渲染之前完成这些操作

useMemo

useMemo 是 React 中的一个 Hook,用于优化性能。它允许你在依赖项不变的情况下缓存计算结果,从而避免重复执行昂贵的计算或创建复杂的对象。这对于提升应用性能特别有用,尤其是在计算密集型任务或渲染大量数据时。

基本概念

useMemo 的主要作用是:

  • 缓存计算结果:在依赖项没有变化时,返回之前缓存的结果,而不是重新计算。
  • 减少不必要的计算:避免每次组件重新渲染时都执行相同的计算。

基本语法

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • 第一个参数:一个函数,该函数返回需要缓存的值。
  • 第二个参数:一个依赖项数组,当这些依赖项发生变化时,useMemo 会重新执行传入的函数并更新缓存值;否则,它将返回之前的缓存值。

使用场景

以下是一些适合使用 useMemo 的典型场景:

  • 昂贵的计算:当你有一个需要进行大量计算的函数,并且希望在依赖项不变的情况下避免重复计算。
  • 复杂对象的创建:当你需要创建一个复杂的对象(如数组、对象字面量等),并且希望在依赖项不变的情况下避免重新创建。
  • 避免不必要的渲染:当你的组件依赖于某个计算结果,并且该结果的变化会导致子组件重新渲染时,使用 useMemo 可以避免不必要的重新渲染。

注意事项

  • 不要滥用 useMemo:虽然 useMemo 可以帮助优化性能,但过度使用可能会导致代码复杂性增加,并且在某些情况下反而会影响性能(例如,频繁地创建和销毁依赖项)。因此,建议只在确实有性能瓶颈的地方使用 useMemo
  • 依赖项数组:确保依赖项数组包含所有可能影响计算结果的变量。如果遗漏了某个依赖项,可能会导致缓存值不正确。

自定义 Hooks

import { useState, useEffect } from 'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch(url)
      .then(response => response.json())
      .then(setData)
      .catch(setError);
  }, [url]);

  return { data, error };
}

// 使用自定义 Hook
function DataFetcher({ url }) {
  const { data, error } = useFetch(url);

  if (error) return <div>Error: {error.message}</div>;
  return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}

具体的应用场景

  • 数据获取: 使用 useEffect 和 useState 来从 API 获取数据。
  • 表单管理: 使用 useState 来管理输入框的状态,并处理提交。
  • 动画: 使用 useEffect 来设置动画的入场和离场。
  • 订阅: 在 useEffect 中添加订阅,返回一个清理函数以取消订阅。
  • 组合逻辑: 使用自定义 Hooks 来提取组件之间共享的逻辑。

组件传参

子组件接收到的 props 是不可变的。如果需要修改 props 中的数据,应该通过父组件来管理状态并传递更新的值。

1 - 4 都是 父传子, 5 是 子传父

传递常见数据类型(类组件写法)

在父组件的 state 中存储基本数据类型,并通过 props 传递给子组件

//父组件
import React, { Component } from 'react';
import ChildComponent from './ChildComponent';

class ParentComponent extends Component {
  state = {
  	// 基本数据类型
    message: 'Hello from Parent!',
    numberValue: 42,
    boolValue: true,
    
    user: {	// 对象
      name: 'Alice',
      age: 30,
    },
    
  };

  handleAlert = () => {	// 方法
    alert('Hello from Parent!');
  };

  updateBoolValue = () => {
    this.setState(prevState => ({ boolValue: !prevState.boolValue }));
  };

  render() {
    return (
      <div>
        <h1>Parent Component</h1>
        <ChildComponent
          message={this.state.message}
          number={this.state.numberValue}
          isTrue={this.state.boolValue}
          
          user={this.state.user}	// 对象
          showAlert={this.handleAlert}	// 方法
        />
        <button onClick={this.updateBoolValue}>Toggle Boolean</button>
      </div>
    );
  }
}

export default ParentComponent;

//子组件
import React, { Component } from 'react';

class ChildComponent extends Component {
  render() {
    return (
      <div>
        <h2>Child Component</h2>
        <p>{this.props.message}</p>
        <p>Number: {this.props.number}</p>
        <p>Boolean: {this.props.isTrue ? 'True' : 'False'}</p>
        
        <ChildComponent user={this.state.user} />	// 对象
      </div>
    );
  }
}

export default ChildComponent;

传递常见数据类型(函数组件写法)

//父组件
import React, { useState } from 'react';
import ChildComponent from './ChildComponent';

const ParentComponent = () => {
  const [message] = useState('Hello from Parent!');
  const [numberValue] = useState(42);
  const [boolValue, setBoolValue] = useState(true);
  const [user] = useState({
    name: 'Alice',
    age: 30,
  });

  const handleAlert = () => {
    alert('Hello from Parent!');
  };

  const updateBoolValue = () => {
    setBoolValue(prev => !prev);
  };

  return (
    <div>
      <h1>Parent Component</h1>
      <ChildComponent
        message={message}
        number={numberValue}
        isTrue={boolValue}
        user={user}
        showAlert={handleAlert}
      />
      <button onClick={updateBoolValue}>Toggle Boolean</button>
    </div>
  );
};

export default ParentComponent;

//子组件
import React from 'react';

const ChildComponent = ({ message, number, isTrue, user }) => {
  return (
    <div>
      <h2>Child Component</h2>
      <p>{message}</p>
      <p>Number: {number}</p>
      <p>Boolean: {isTrue ? 'True' : 'False'}</p>
      <p>User: {user.name}, Age: {user.age}</p> {/* 对象 */}
    </div>
  );
};

export default ChildComponent;

传递多个 HTML 结构(类组件)

// 父组件将多个 JSX 元素传递给子组件,通过 `props.children` 访问这些元素。

import React, { Component } from 'react';
import ChildComponent from './ChildComponent';

class ParentComponent extends Component {
  render() {
    return (
      <div>
        <h1>Parent Component</h1>
        <ChildComponent>	//  把 html 写在组件子组件当中
          <p>This is the first custom content!</p>
          <p>This is the second custom content!</p>
          <h3>This is a header as custom content!</h3>
        </ChildComponent>
      </div>
    );
  }
}

export default ParentComponent;

// 在子组件中,我们可以通过 React.Children.map 或者 this.props.children 分别处理传递过来的多个元素
import React, { Component } from 'react';

class ChildComponent extends Component {
  render() {
    return (
      <div>
        <h2>Child Component</h2>	// children数组中包含了父组件传递的 html
        {React.Children.map(this.props.children, (child, index) => (
          <div key={index}>{child}</div>
        ))}
      </div>
    );
  }
}

export default ChildComponent;

传递多个 HTML 结构(函数组件)

// 父组件
import React from 'react';
import ChildComponent from './ChildComponent';

const ParentComponent = () => {
  return (
    <div>
      <h1>Parent Component</h1>
      <ChildComponent>
        <p>This is the first custom content!</p>
        <p>This is the second custom content!</p>
        <h3>This is a header as custom content!</h3>
      </ChildComponent>
    </div>
  );
};

export default ParentComponent;

//子组件
import React from 'react';

const ChildComponent = ({ children }) => {
  return (
    <div>
      <h2>Child Component</h2>
      {React.Children.map(children, (child, index) => (
        <div key={index}>{child}</div>
      ))}
    </div>
  );
};

export default ChildComponent;

// 或者子组件
import React from 'react';

const ChildComponent = ({ children }) => {
  return (
    <div>
      <h2>Child Component</h2>
      { children }
    </div>
  );
};

export default ChildComponent;

React.Children.map

React.Children.map 是 React 提供的一个实用工具函数,用于安全地遍历和操作 children prop。它特别适用于处理不确定数量或类型的子元素,确保即使 childrennullundefined 时也不会抛出错误

基本用法

React.Children.map 函数接受两个参数:

  • 1. children: 需要遍历的子元素。

  • 2. function: 对每个子元素执行的回调函数。 回调函数会接收两个参数:

    • child: 当前遍历到的子元素。
    • index: 当前子元素的索引。

子传父

  • 父组件: 使用 useState 来存储从子组件接收到的数据,并定义一个 handleDataChange 函数来更新状态。
  • 子组件: 接收 onDataChange 函数作为 prop,点击按钮时调用该函数将数据发送给父组件
// 父组件
import React, { useState } from 'react';
import ChildComponent from './ChildComponent';

const ParentComponent = () => {
  const [dataFromChild, setDataFromChild] = useState('');

  const handleDataChange = (data) => {
    setDataFromChild(data);
  };

  return (
    <div>
      <h1>Parent Component</h1>
      <p>Data from Child: {dataFromChild}</p>
      <ChildComponent onDataChange={handleDataChange} />
    </div>
  );
};

export default ParentComponent;

// 子组件
import React from 'react';

const ChildComponent = ({ onDataChange }) => {
  const sendDataToParent = () => {
    const data = 'Hello from Child!';
    onDataChange(data); // 调用父组件传来的函数
  };

  return (
    <div>
      <h2>Child Component</h2>
      <button onClick={sendDataToParent}>Send Data to Parent</button>
    </div>
  );
};

export default ChildComponent;

状态管理

在复杂的应用中,状态管理变得尤为重要。React 提供了几种不同的状态管理解决方案,包括但不限于:

Redux

Redux 是一个用于管理应用状态的库,常与 React 一起使用。它通过一个全局的 store 来集中管理应用的状态。

核心概念

  1. State(状态)

    • Redux 应用中的状态被集中存储在一个单一的 store 中。
    • 这个 store 是一个包含整个应用状态的对象树。
  2. Action(动作)

    • Actions 是用来描述发生了什么的普通对象。
    • 它们至少需要一个 type 属性来表示动作的类型。
    • 可选地,actions 可以携带额外的数据作为 payload。
  3. Reducer(归约器)

    • Reducers 是纯函数,它们接收当前的 state 和 action,并返回新的 state。
    • 它们不修改现有的 state,而是根据传入的 action 创建并返回一个新的 state。
  4. Store(仓库)

    • Store 是保存应用状态的地方。
    • 它提供方法来获取状态 (getState)、派发动作 (dispatch) 和注册监听器 (subscribe)。
  • 安装 Redux

    npm install redux react-redux
    
  • 创建 Store

    // 导入 Redux
    import { createStore } from 'redux';
    // 定义初始状态
    const initialState = {
      counter: 0
    };
    // 创建 Reducer
    // Reducer 是一个纯函数,接受当前状态和 action,并返回新的状态
    const reducer = (state = initialState, action) => {
    switch (action.type) {
      case 'INCREMENT':
        return { ...state, counter: state.counter + 1 };
      case 'DECREMENT':
        return { ...state, counter: state.counter - 1 };
      default:
        return state;
      }
    }
    
    const store = createStore(reducer)
    
  • 连接组件

    import React from 'react';
    import { useSelector, useDispatch } from 'react-redux';
    
    function Counter() {
      const counter = useSelector(state => state.counter);
      const dispatch = useDispatch();
    
      return (
        <div>
          <p>Counter: {counter}</p>
          <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
          <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
        </div>
      )
    }
    
    export default Counter
    

Redux Toolkit

Redux Toolkit 是官方推荐的 Redux 工具集,它简化了 Redux 的使用,包含了一些开箱即用的工具和最佳实践。使用 Redux Toolkit 可以更容易地管理状态,减少模板代码

安装

npm install @reduxjs/toolkit react-redux

创建 Slice

使用 createSlice 来定义 state、reducers 和 actions。这样可以简化代码结构。

import { createSlice } from '@reduxjs/toolkit';

const todosSlice = createSlice({
  name: 'todos',
  initialState: [], // 要存的state

  // 使用 createSlice 创建 slice 时,定义的 reducer 函数会自动生成对应的 action creators。
  reducers: {
    addTodo: (state, action) => {	// state中包含所有 initialState 中的数据, 
      state.push(action.payload); // 使用 Immer 库来处理不可变状态
    },
    removeTodo: (state, action) => {
      return state.filter(todo => todo.id !== action.payload);	// payload 包含了调用时传递的参数, 可以是对象
    },
  },
});

// 导出 action
export const { addTodo, removeTodo } = todosSlice.actions;

// 导出 reducer
export default todosSlice.reducer;

配置 Store

import { configureStore } from '@reduxjs/toolkit';
import todosReducer from './todosSlice';

const store = configureStore({
  reducer: {
    todos: todosReducer,
  },
});

export default store;

连接 React 和 Redux

在应用中使用 Provider 将 store 传递给组件树。

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

使用 Redux State

在组件中使用 useSelectoruseDispatch 来访问状态和发起 action

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { addTodo, removeTodo } from './todosSlice';

const TodoList = () => {
  const todos = useSelector(state => state.todos);
  const dispatch = useDispatch();

  const handleAddTodo = () => {
    const text = prompt('Enter todo:');
    const id = Date.now(); // 简单生成 ID
    dispatch(addTodo({ id, text }));
  };

  const handleRemoveTodo = (id) => {
    dispatch(removeTodo(id));
  };

  return (
    <div>
      <h1>Todo List</h1>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            {todo.text}
            <button onClick={() => handleRemoveTodo(todo.id)}>Remove</button>
          </li>
        ))}
      </ul>
      <button onClick={handleAddTodo}>Add Todo</button>
    </div>
  );
};

export default TodoList;

处理异步操作

使用 createAsyncThunk 处理异步请求

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

// 异步操作示例
export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
  const response = await fetch('/api/todos');
  return response.json();
});

const todosSlice = createSlice({
  name: 'todos',
  initialState: { items: [], status: 'idle' },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchTodos.fulfilled, (state, action) => {
        state.items = action.payload;
      });
  },
});

// 导出 reducer
export default todosSlice.reducer;

Mobx

MobX 是一个简单、透明的状态管理库,它通过可观察对象和反应式视图来管理状态

  • 安装 MobX

    npm install mobx mobx-react
    
  • 创建 Store

    import { makeAutoObservable } from 'mobx';
    
    class CounterStore {
      counter = 0;
    
      constructor() {
        makeAutoObservable(this);
      }
    
      increment = () => {
        this.counter += 1;
      }
    
      decrement = () => {
        this.counter -= 1;
      }
    }
    
    const counterStore = new CounterStore()
    
  • 连接组件

    import React from 'react';
    import { observer } from 'mobx-react';
    import { counterStore } from './stores/CounterStore';
    
    const Counter = observer(() => {
      return (
        <div>
          <p>Counter: {counterStore.counter}</p>
          <button onClick={counterStore.increment}>Increment</button>
          <button onClick={counterStore.decrement}>Decrement</button>
        </div>
      )
    })
    
    export default Counter
    

React Context API

React 的 Context API 也可以用于状态管理,特别是在中小型应用中。虽然它不如 Redux 和 MobX 强大,但对于简单的状态管理需求已经足够。

  • 创建 Context

    import React, { createContext, useState } from 'react';
    
    const CounterContext = createContext();
    
    const CounterProvider = ({ children }) => {
      const [counter, setCounter] = useState(0);
    
      const increment = () => setCounter(counter + 1);
      const decrement = () => setCounter(counter - 1);
    
      return (
        <CounterContext.Provider value={{ counter, increment, decrement }}>
          {children}
        </CounterContext.Provider>
      )
    }
    
    export { CounterContext, CounterProvider }
    
  • 使用 Context

    import React, { useContext } from 'react';
    import { CounterContext } from './CounterContext';
    
    const Counter = () => {
      const { counter, increment, decrement } = useContext(CounterContext);
    
      return (
        <div>
          <p>Counter: {counter}</p>
          <button onClick={increment}>Increment</button>
          <button onClick={decrement}>Decrement</button>
        </div>
      )
    }
    
    export default Counter
    

React Router路由

简介

React Router 是 React 生态中最流行的路由库,用于管理应用的路由和导航。它支持以下功能:

  • 声明式路由配置。
  • 动态路由匹配。
  • 嵌套路由。
  • 路由守卫(权限控制)。
  • 懒加载路由。

安装React Router

npm install react-router-dom

路由配置

配置路由通常涉及创建一个路由表,定义各个路由及其对应的组件。

路由容器

  • BrowserRouter:基于 HTML5 History API,路径如 /about。
  • HashRouter:基于 URL Hash,路径如 /#/about,兼容性更好。
import { BrowserRouter } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      {/* 其他组件和路由配置 */}
    </BrowserRouter>
  );
}

路由定义与匹配

使用 RoutesRoute 组件定义路由规则:

关键点:

  • path:匹配 URL 路径,支持动态参数(:id)。
  • element:渲染的组件。
  • *:通配符路径,用于 404 页面。
import { Routes, Route } from 'react-router-dom';

function App() {
  return (
    <Routes>
      {/* 精确匹配路径 */}
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/users/:userId" element={<UserProfile />} />
      <Route path="*" element={<NotFound />} /> {/* 404 页面 */}
    </Routes>
  );
}

常用组件

BrowserRouter

这个组件提供了基于 HTML5 的历史记录的路由。它通常包裹在应用的根组件外部。

import { BrowserRouter } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      {/* 其他组件 */}
    </BrowserRouter>
  );
}

Route

用于定义路由的组件。它接受 path 属性来指定匹配的 URL,以及 element 属性来指定渲染的组件

import { Route } from 'react-router-dom';

<Route path="/about" element={<About />} />

Routes

用来包裹多个 Route 组件,确保只有一个匹配的路由被渲染

import { Routes } from 'react-router-dom';

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
</Routes>

Link

用于在应用中创建导航链接的组件。它类似于 HTML 的 <a> 标签,但使用的是客户端路由。使用 Link 组件实现导航,避免页面刷新。

  • to:指定目标路径。
import { Link } from 'react-router-dom';

<Link to="/about">About</Link>

NavLink 组件

NavLink 是一个特殊的 Link 组件,它可以知道当前是否是“激活”状态(即当前页面与链接匹配)。当链接被激活时,NavLink 可以自动应用特定的样式或类名,这对于实现高亮当前导航项非常有用

支持高亮当前活动路由

默认情况下,当 NavLink 对应的路径是当前路径时,它会自动添加一个名为 active 的类名

<NavLink to="/" >NavLink链接</NavLink>
/**
如果当前路由为"/"时,上述代码会被编译为下述代码,会自动添加class="active"
<a aria-current="page" class="active" href="/" data-discover="true">NavLink链接</a>
当前路由不是"/"时,上述代码会被编译为下述代码,不会自动添加类名
<a class="" href="/" data-discover="true">NavLink链接</a>
*/
动态样式

为了根据链接是否激活来动态改变样式,你可以利用 classNamestyle 属性提供的函数形式:

在 React Router v6 中,NavLink 组件可以通过传递一个函数给 classNamestyle 属性来动态地设置类名或样式,基于链接是否激活。

<NavLink to="/" style={params => console.log("params:", params)}>NavLink链接</NavLink>

/*
会发现params是这样的,{isActive: true, isPending: false, isTransitioning: false},其中isActive表示当前链接是否为激活态,从而动态生成className或style
*/
  • 使用 className 函数

    // 如果链接是激活状态,则会应用 active-link 类;否则,将应用 inactive-link 类
    <NavLink
      to="/home"
      className={({ isActive }) => 
        isActive ? 'active-link' : 'inactive-link'
      }
    >
      Home
    </NavLink>
    
  • 使用 style 函数

    // 如果你想直接通过内联样式来改变链接的外观,可以这样做。这会让激活状态下的链接文字变为红色且字体加粗,未激活状态下则为蓝色且字体正常
    <NavLink
      to="/home"
      style={({ isActive }) => ({
        color: isActive ? 'red' : 'blue',
        fontWeight: isActive ? 'bold' : 'normal',
      })}
    >
      Home
    </NavLink>
    
精确匹配

有时候,你可能希望只有在路径完全匹配时才认为链接是激活状态。在这种情况下,你可以使用 end 属性(v6 版本):

<NavLink to="/" end>Home</NavLink>
自定义封装

为了减少重复代码,你可以对 NavLink 进行封装。例如,创建一个 MyNavLink 组件,为所有 NavLink 添加统一的样式或属性:

const MyNavLink = ({ children, ...props }) => (
  <NavLink {...props} className={({ isActive }) => isActive ? 'selected' : ''}>
    {children}
  </NavLink>
);

// 使用自定义的 MyNavLink 组件
<MyNavLink to="/home">Home</MyNavLink>

注意事项

  • 移除的属性:在 v6 版本中,activeClassNameactiveStyle 已经被移除。如果你需要基于激活状态来调整样式,应该使用上述提到的 classNamestyle 函数。
  • 精确匹配:对于根路径 (/) 的链接,如果你不希望其子路径也被视为激活状态,记得添加 end 属性

Navigate

用于在组件中进行重定向。to:指定目标路径。

import { Navigate } from 'react-router-dom';

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/old" element={<Navigate to="/new" />} />
      <Route path="/new" element={<NewPage />} />
    </Routes>
  );
}

案例:

import React from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

// 导入页面组件
import Home from './Home';
import About from './About';
import NotFound from './NotFound';

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        {/* 404 页面 */}
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

嵌套路由

嵌套路由允许你在某个父路由的组件内部定义子路由。这样,你可以在父路由的页面中渲染子路由对应的内容。

<Route path="/users" element={<Users />}>
  <Route path=":id" element={<UserDetail />} />
  <Route path="new" element={<NewUser />} />
</Route>

在父组件中使用 Outlet 渲染子路由:

import { Outlet } from 'react-router-dom';

function Users() {
  return (
    <div>
      <h1>Users Page</h1>
      <Outlet /> {/* 子路由将渲染在这里 */}
    </div>
  );
}

默认路由

使用 index 定义默认路由(当父路由匹配时渲染的默认子路由)。

<Route path="/users" element={<Users />}>
  <Route index element={<UserList />} /> {/* 默认渲染 UserList */}
  <Route path=":id" element={<UserDetail />} />
</Route>

注意事项

  • zpath 和 index 的使用:在嵌套路由配置中,如果一个子路由要作为默认路由,你应该只设置 index 属性而不需要指定 path。当你给 index 路由添加了 path 属性时,它实际上变成了一个普通的子路由而不是默认路由。正确的做法是移除 path 属性,并仅保留 index 属性。
  • 父组件中的 <Outlet>:确保父组件 (Demo) 内部包含 <Outlet> 组件,这样才能让子路由的内容得以显示。

路由守卫

路由保护可以确保只有特定条件下(例如用户登录后)才能访问某些路由。可以通过创建一个高阶组件(HOC)来实现。

import {
  BrowserRouter as Router,
  Route,
  Routes,
  Navigate,
} from "react-router-dom";
import Login from "./pages/login/login";
import Home from "./pages/home/home";
 
function App() {
  const routeGuard = (components, path) => {
    if (path == "/login") {
      return components;
    } else {
      return localStorage.getItem("token") ? (
        components
      ) : (
        <Navigate to="/login" />
      );
    }
  };
  return (
      <Router>
        <Routes>
          <Route path="/" element={<Navigate to="/home" />} />
          <Route path="/login" element={<Login />}></Route>
          <Route path="/home" element={routeGuard(<Home/>, "home")}></Route>
        </Routes>
      </Router>
  );
}
 
export default App;

404 页面处理

可以使用通配符路由 * 来处理未匹配的路径,并显示 404 页面。

<Route path="*" element={<NotFound />} />

动态路由(match)

使用 :param 语法定义动态路由参数。

动态路由允许你在路径中使用参数,这样可以在 URL 中捕获动态值。例如,展示用户信息的页面。

<Route path="/users/:userId" element={<UserProfile />} />

获取URL参数

类组件

使用 URL 参数,可以使用 withRouter 高阶组件来获取路由的 props,包括 matchlocationhistory 对象。

  • withRouter 是一个高阶组件,它将路由的相关信息作为 props 传递给被包装的组件。
  • match.params 中包含了 URL 中的动态参数,像这里的 userId
import React from 'react';
import { withRouter } from 'react-router-dom';
class UserProfile extends React.Component {
  render() {
    // 从 props 中获取 match 对象
    const { match } = this.props;
    const userId = match.params.userId; // 获取 URL 参数

    return <div>User ID: {userId}</div>;
  }
}
// 使用 withRouter 将 UserProfile 包装起来
export default withRouter(UserProfile);


// 确保在路由配置中使用 UserProfile 组件时,像这样设置路由:
<Route path="/users/:userId" element={<UserProfile />} />
函数组件

直接使用 useParams 来获取 URL 参数

import { useParams } from 'react-router-dom';

function UserProfile() {
  const { userId } = useParams();
  return <div>User ID: {userId}</div>;
}

获取查询字符串

类组件

在类组件中,我们通常使用 withRouter 高阶组件来访问路由相关的信息,包括查询字符串。

import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';

class QueryParamsClassComponent extends Component {
  render() {
    const { location } = this.props; // 从 props 中获取 location 对象
    const searchParams = new URLSearchParams(location.search); // 创建 URLSearchParams 对象

    // 获取具体的参数
    const userId = searchParams.get('userId'); // 获取 userId 参数
    const name = searchParams.get('name'); // 获取 name 参数
    // searchParams。set('userId', '456'); // 修改或添加 userId 参数
    // searchParams.set('name', 'Alice'); // 修改或添加 name 参数

    return (
      <div>
        <h1>Query Parameters</h1>
        <p>User ID: {userId}</p>
        <p>Name: {name}</p>
      </div>
    );
  }
}

export default withRouter(QueryParamsClassComponent); // 使用 withRouter 包裹类组件
函数组件

使用 useSearchParams 处理查询参数

当你在组件中使用 useSearchParams 时,它会返回一个数组,数组包含下面两个元素:

  • 当前的 URLSearchParams 对象:这是一个包含所有查询参数的对象,可以通过 .get().getAll() 等方法来访问这些参数。
  • 设置新的查询参数的函数:这是用来更新查询参数的方法,可以用来添加、删除或更改现有的查询参数。(useSearchParams.set后,这个操作实际上并不会自动反映到 URL 上。这是因为 URLSearchParams 对象是不可变的(immutable),你需要使用 setSearchParams 函数来触发 URL 的更新)
第一个参数:URLSearchParams 对象
  • 获取查询参数值

    • get(name): 返回指定名称的第一个查询参数值。

      const [searchParams] = useSearchParams();
      const value = searchParams.get('name'); // 返回 'name' 参数的第一个值
      
    • getAll(name): 返回指定名称的所有查询参数值(如果存在重复键)。

      const values = searchParams.getAll('name'); // 返回所有 'name' 参数的值
      
  • 检查是否存在某个查询参数

    • has(name): 如果查询字符串中有给定名称的参数,则返回 true

      const exists = searchParams.has('name'); // 如果有 'name' 参数则返回 true
      
  • 添加或修改查询参数

    • set(name, value): 设置查询参数的值。如果参数已经存在,则更新其值;如果不存在,则添加新的参数。

      searchParams.set('name', 'value'); // 更新或添加 'name' 参数
      
  • 删除查询参数

    • delete(name): 从查询字符串中删除指定名称的参数。

      searchParams.delete('name'); // 删除 'name' 参数
      
  • 遍历查询参数

    • forEach(callbackFn[, thisArg]): 对每个查询参数执行一次提供的函数。

      searchParams.forEach((value, key) => {
        console.log(key + ': ' + value);
      });
      
    • entries(): 返回一个新的迭代器对象,该迭代器将遍历所有的键/值对。

      for (const [key, value] of searchParams.entries()) {
        console.log(key + ': ' + value);
      }
      
  • 其他方法

    • keys(): 返回一个新的迭代器对象,该迭代器将遍历所有的键。

      • values(): 返回一个新的迭代器对象,该迭代器将遍历所有的值。
      • sort(): 对查询参数按字典顺序排序。
setSearchParams参数说明
  • nextInit: 这是一个对象、字符串或 URLSearchParams 实例,表示你想要设置的新查询参数。如果传递的是一个对象,那么所有已有的查询参数将被这个对象中的参数替换,除非你手动保留了原有的参数。

  • navigateOptions: 这是一个可选的对象,可以包含两个属性:

    • replace: 如果设置为 true,则会替换当前的历史记录条目而不是创建一个新的历史记录条目。
    • state: 允许你向浏览器历史堆栈中添加额外的状态信息。
import React from 'react';
import { useSearchParams } from 'react-router-dom';

export default function SearchPage() {
    const [searchParams, setSearchParams] = useSearchParams();

    const handleSearch = () => {
        setSearchParams({ keyword: 'example', page: 1 }, { replace: true });
    };

    return (
        <div>
            <p>Search Keyword: {searchParams.get('keyword')}</p>
            <p>Page: {searchParams.get('page')}</p>
            <button onClick={handleSearch}>Search</button>
        </div>
    );
}

Link、NavLink中传递参数

路径参数
  • 传递

    // to为字符串
    <Link to={`/user/${userId}`}>Go to User Profile</Link>
    // to为对象
    <Link to={{ pathname: `/user/${userId}` }}>Go to User Profile</Link>
    
查询参数
  • 传递

    // to为字符串
    <Link to='/search??query=React+Router'>
      Search for React Router
    </Link>
    // to为对象
    <Link to={{ pathname: '/search', search: '?query=React+Router' }}>
      Search for React Router
    </Link>
    
状态参数(State)

通过 state 传递复杂数据

状态参数不会出现在 URL 中,但可以在导航过程中传递额外的信息。这些信息可以通过 useLocation Hook 在目标组件中访问

  • 传递
// to为字符串
<Link to="/details" state={{ from: 'home', id: 123 }}>Details</Link>
// to为对象
<Link
  to={{
    pathname: '/details',
    state: { detailId: 123, otherInfo: 'some data' }
  }}
>
  Go to Details Page
</Link>

在目标组件中通过 useLocation 获取:

useLocation 是 React Router 提供的一个 Hook,它允许你在函数组件中访问当前的 location 对象。这个对象包含了关于当前 URL 的信息,比如路径名、查询参数、哈希等。

location 对象通常包含以下属性 :
  • pathname: 当前路径名。
  • search: 查询字符串部分,包括起始的问号。
  • hash: 哈希片段部分,包括起始的井号。
  • state: 从导航到此位置传递的状态数据。
  • key: 一个唯一的标识符,用于 location 对象。
import { useLocation } from 'react-router-dom';

function DetailsPage() {
  const location = useLocation();
  const { from, id } = location.state || {}; // 如果直接刷新页面,location.state 可能为 null,所以需要处理这种情况。
  return <div>From: {from}, ID: {id}</div>;
}

编程式导航

在 React Router v6 中,编程式导航主要通过 useNavigate Hook 实现。这个 Hook 替代了之前的 useHistory,并提供了一种简单的方法来实现页面跳转和其他导航操作。

使用 useNavigate

useNavigate 返回一个函数,你可以调用它来导航到不同的路由。

这个函数可以接受一个或两个参数,第一个参数是必需的,表示目标路径,第二个参数是一个可选的对象,用于配置导航行为。

参数详解
目标路径(To)
  • 类型:字符串 | 对象
  • 描述:这是你要导航到的目标路径。它可以是一个简单的字符串,如 /home,也可以是一个包含更多细节的对象。
  • 如果是对象形式,它可以包含以下属性:
    • pathname: 要导航到的路径名。
    • search: 查询字符串,例如 ?name=John&age=30
    • hash: URL 中的哈希部分,例如 #section1
    • state: 传递给目标位置的状态数据。

选项(Options)

  • 类型:对象

  • 描述:这是一个可选的对象,用于配置导航的行为。它可能包括以下属性:

    • replace: 布尔值,默认为 false。如果设置为 true,则使用 history.replace() 方法而不是 history.push() 方法进行导航。这意味着新的页面不会被添加到浏览器的历史记录中,而是替换当前的历史记录条目。
    • state: 任何类型的值。这个值会被附加到目标位置,并可以通过 useLocation 钩子在目标组件中访问。
    • relative: 可选的布尔值或数字,指定是否使用相对路径。如果你需要相对于当前位置进行导航,可以使用此选项。
基本使用
import { useNavigate } from 'react-router-dom';

function MyComponent() {
  const navigate = useNavigate();

  function handleClick() {
    // 导航到 /path/to/navigate
    navigate('/path/to/navigate');
  }

  return <button onClick={handleClick}>Go to another page</button>;
}
替换当前历史记录条目

如果你不希望在浏览器的历史记录栈中添加新的条目(即替换当前条目),可以设置 replace 选项为 true

navigate('/path/to/navigate', { replace: true });

传递状态

你还可以向目标位置传递一些状态信息:

navigate('/path/to/navigate', { state: { someData: 'some value' } });

前进/后退导航

// 后退 1 步
navigate(-1);

// 前进 1 步
navigate(1);

相对路径跳转

基于当前路径的相对跳转:

// 当前路径: /users/123
navigate('profile'); // 跳转到 /users/123/profile
navigate('../settings'); // 跳转到 /users/settings

动态路径拼接

使用模板字符串动态生成路径:

const userId = 456;
navigate(`/users/${userId}/details`);

注意事项

  • navigate 函数只能在函数组件或自定义 Hooks 中使用,不能直接在类组件中使用。
  • 当使用 navigate 进行导航时,如果需要在导航后立即执行某些操作,可以在 navigate 调用后使用 .then() 方法,因为 navigate 是异步的。

路由懒加载

结合 React.lazySuspense 实现按需加载:

import { lazy, Suspense } from 'react';

const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
  );
}

路由预加载

在用户可能访问的路径上提前加载组件:

// 鼠标悬停时预加载
<Link to="/about" onMouseEnter={() => import('./About')}>About</Link>

常见问题

路由跳转后页面不更新

  • 原因:组件未正确监听路由变化。
  • 解决:使用 useEffect 监听 location 变化
import { useLocation } from 'react-router-dom';

function MyComponent() {
  const location = useLocation();

  useEffect(() => {
    // 路由变化时执行操作
  }, [location]);
}

动态路由参数变化组件不更新

  • 原因:组件未监听参数变化。
  • 解决:在组件内通过 useParams 获取参数,并触发更新:
function UserDetail() {
  const { userId } = useParams();
  useEffect(() => {
    // 根据 userId 加载数据
  }, [userId]);
}

滚动位置恢复

  • 方案:使用 useScrollRestoration(需自定义实现)或第三方库。

完整路由配置示例

定义路由

// src/routes.js

import React from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';

// 引入页面组件
import HomePage from './pages/HomePage';
import AboutPage from './pages/AboutPage';
import ContactPage from './pages/ContactPage';
import NotFoundPage from './pages/NotFoundPage';
import LoginPage from './pages/LoginPage';

// 定义私有路由(需要登录的页面)
const PrivateRoute = ({ children }) => {
  const isAuthenticated = localStorage.getItem('authToken'); // 假设通过 token 判断用户是否已登录

  return isAuthenticated ? children : <Navigate to="/login" />;
};

// 定义公共路由(不需要登录的页面)
const PublicRoute = ({ children }) => {
  const isAuthenticated = localStorage.getItem('authToken');

  return isAuthenticated ? <Navigate to="/" /> : children;
};

const RoutesComponent = () => (
  <Routes>
    {/* 首页 */}
    <Route path="/" element={<HomePage />} />

    {/* 关于我们页面 */}
    <Route path="/about" element={<AboutPage />} />

    {/* 联系我们页面 */}
    <Route path="/contact" element={<ContactPage />} />

    {/* 登录页面 */}
    <Route path="/login" element={
      <PublicRoute>
        <LoginPage />
      </PublicRoute>
    } />

    {/* 需要登录后才能访问的页面 */}
    <Route path="/dashboard" element={
      <PrivateRoute>
        <div>
          <h1>Dashboard</h1>
          <p>Welcome to your dashboard!</p>
        </div>
      </PrivateRoute>
    } />

    {/* 404 页面 */}
    <Route path="*" element={<NotFoundPage />} />
  </Routes>
);

export default RoutesComponent;

使用 routes.js 文件

在你的 App.js 文件中引入并使用 RoutesComponent

// src/App.js

import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import RoutesComponent from './routes';

function App() {
  return (
    <Router>
      <RoutesComponent />
    </Router>
  );
}

export default App;

性能优化

React.memo

React.memo 是 React 提供的一个高阶组件(Higher-Order Component, HOC),用于优化性能。它的主要作用是通过浅比较 props 来决定是否重新渲染组件。如果 props 没有变化,组件就不会重新渲染,从而避免不必要的渲染操作,提升应用的性能。

const MyComponent = React.memo(function MyComponent(props) {
  /* 渲染使用 props 的 UI */
})

useMemo

用于缓存计算结果,避免重复计算

function MyComponent(props) {
  const memoizedValue = useMemo(() => computeExpensiveValue(props), [props]);
  // ...
}

错误边界

错误边界是一种 React 组件,它能够捕获在其子组件树中的任何位置发生的 JavaScript 错误,并显示一个备用 UI 而不是让整个应用崩溃。

错误边界仅能捕获发生在组件生命周期方法(如 render、componentDidMount 等)和构造函数中的错误,而不能捕获事件处理程序中的异步代码错误(如 setTimeout 或 fetch 请求)。

  • 定义边界错误
    要创建一个错误边界组件,你需要使用两个生命周期方法:

    • static getDerivedStateFromError(error):这个静态方法会在后代组件抛出错误后被调用。它可以返回一个对象来更新组件的状态。
    • componentDidCatch(error, info):这个方法会在后代组件抛出错误后被调用。它可以用于记录错误信息。
    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props);
        this.state = { hasError: false };
      }
    
      static getDerivedStateFromError(error) {
        return { hasError: true };
      }
    
      componentDidCatch(error, errorInfo) {
        logErrorToMyService(error, errorInfo);
      }
    
      render() {
        if (this.state.hasError) {
          return <h1>Something went wrong.</h1>;
        }
    
        return this.props.children; 
      }
    }
    
  • 使用边界错误

    <ErrorBoundary>
      <MyWidget />
    </ErrorBoundary>
    

注意事项

  1. 错误边界的局限性

    • 错误边界无法捕获以下类型的错误:

      • 事件处理程序中的错误(例如 onClick
      • 异步代码(例如 setTimeoutrequestAnimationFrame
      • 服务端渲染期间的错误
      • 错误边界本身抛出的错误(这会导致整个应用崩溃)
  2. 最佳实践

    • 尽量将错误边界放置在靠近顶层的位置,以便捕获尽可能多的错误。
    • 在开发环境中,考虑使用工具(如 React.StrictMode)来帮助发现潜在的错误。
    • 记录错误日志并将它们发送到监控系统,以便更好地进行调试和维护。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值