react:组件基础

这是本文档旧的修订版!


第五章:React 组件基础

函数组件是定义 React 组件最简单的方式。它是一个接收 props 对象并返回 React 元素的 JavaScript 函数。

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

特点

  • 简洁:纯函数,没有生命周期方法和 this
  • 无状态(Hooks 之前):只能接收 props,不能管理自己的 state
  • 性能:没有实例化开销,渲染更快

使用箭头函数

const Welcome = (props) => {
  return <h1>Hello, {props.name}</h1>;
};
 
// 简写形式(隐式返回)
const Welcome = (props) => <h1>Hello, {props.name}</h1>;
 
// 解构 props
const Welcome = ({ name }) => <h1>Hello, {name}</h1>;

类组件使用 ES6 的 class 语法定义,继承自 React.Component。

import React from 'react';
 
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

特点

  • 可以使用 state 管理组件内部状态
  • 可以使用生命周期方法
  • 有 this 关键字
  • 需要 render() 方法返回 JSX

组件可以相互组合,形成组件树。

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}
 
function App() {
  return (
    <div>
      <Welcome name="张三" />
      <Welcome name="李四" />
      <Welcome name="王五" />
    </div>
  );
}

提取组件

当 UI 的一部分被多次使用,或者自身足够复杂时,可以将其提取为独立的组件。

// 原始代码
function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <img className="Avatar"
          src={props.author.avatarUrl}
          alt={props.author.name}
        />
        <div className="UserInfo-name">
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}
 
// 提取 Avatar 组件
function Avatar(props) {
  return (
    <img className="Avatar"
      src={props.user.avatarUrl}
      alt={props.user.name}
    />
  );
}
 
// 提取 UserInfo 组件
function UserInfo(props) {
  return (
    <div className="UserInfo">
      <Avatar user={props.user} />
      <div className="UserInfo-name">
        {props.user.name}
      </div>
    </div>
  );
}
 
// 重构后的 Comment 组件
function Comment(props) {
  return (
    <div className="Comment">
      <UserInfo user={props.author} />
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

Props(properties 的缩写)是组件之间传递数据的方式。

传递 props

function App() {
  return <User name="张三" age={25} isAdmin={true} />;
}

接收 props

function User(props) {
  return (
    <div>
      <p>姓名:{props.name}</p>
      <p>年龄:{props.age}</p>
      <p>{props.isAdmin ? '管理员' : '普通用户'}</p>
    </div>
  );
}
 
// 使用解构
function User({ name, age, isAdmin }) {
  return (
    <div>
      <p>姓名:{name}</p>
      <p>年龄:{age}</p>
      <p>{isAdmin ? '管理员' : '普通用户'}</p>
    </div>
  );
}

Props 的只读性

组件不能修改自己的 props。props 对于组件来说是只读的。

// 错误!props 是只读的
function Welcome(props) {
  props.name = 'Hello'; // 错误!
  return <h1>{props.name}</h1>;
}

为 props 设置默认值:

函数组件

function Button({ text = '点击', type = 'button' }) {
  return <button type={type}>{text}</button>;
}
 
// 或者
function Button(props) {
  const { text = '点击', type = 'button' } = props;
  return <button type={type}>{text}</button>;
}

类组件

class Button extends React.Component {
  static defaultProps = {
    text: '点击',
    type: 'button'
  };
 
  render() {
    return <button type={this.props.type}>{this.props.text}</button>;
  }
}
 
// 或者在组件外部
Button.defaultProps = {
  text: '点击',
  type: 'button'
};

PropTypes 用于运行时类型检查。

安装:

npm install prop-types

使用:

import PropTypes from 'prop-types';
 
function User({ name, age, email }) {
  return (
    <div>
      <p>{name}</p>
      <p>{age}</p>
      <p>{email}</p>
    </div>
  );
}
 
User.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number,
  email: PropTypes.string,
  isAdmin: PropTypes.bool,
  hobbies: PropTypes.array,
  address: PropTypes.object,
  onClick: PropTypes.func,
  element: PropTypes.element,
  nodes: PropTypes.node,
  user: PropTypes.instanceOf(User),
  shape: PropTypes.shape({
    color: PropTypes.string,
    fontSize: PropTypes.number
  }),
  oneOf: PropTypes.oneOf(['News', 'Photos']),
  oneOfType: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.number
  ]),
  arrayOf: PropTypes.arrayOf(PropTypes.number),
  objectOf: PropTypes.objectOf(PropTypes.number),
  any: PropTypes.any
};
 
User.defaultProps = {
  age: 18
};

TypeScript 替代方案

如果使用 TypeScript,可以使用接口定义 props 类型,获得编译时类型检查。

children 是一个特殊的 prop,用于在组件标签之间传递内容。

function Card({ title, children }) {
  return (
    <div className="card">
      <div className="card-header">{title}</div>
      <div className="card-body">{children}</div>
    </div>
  );
}
 
// 使用
function App() {
  return (
    <Card title="欢迎使用">
      <p>这是卡片的内容</p>
      <button>了解更多</button>
    </Card>
  );
}

children 的类型

  • React 元素
  • 字符串或数字
  • 数组(Fragment)
  • 函数(render props)
  • null、undefined、boolean(不渲染)

render props 是一种在 React 组件之间使用一个值为函数的 prop 共享代码的技术。

class MouseTracker extends React.Component {
  state = { x: 0, y: 0 };
 
  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  };
 
  render() {
    return (
      <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)}
      </div>
    );
  }
}
 
// 使用
function App() {
  return (
    <MouseTracker render={({ x, y }) => (
      <p>鼠标位置:({x}, {y})</p>
    )} />
  );
}

高阶组件是一个函数,接收一个组件并返回一个新的组件。

function withLogger(WrappedComponent) {
  return class extends React.Component {
    componentDidMount() {
      console.log('Component mounted:', WrappedComponent.name);
    }
 
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}
 
// 使用
const EnhancedComponent = withLogger(MyComponent);

常见的 HOC 用途

  • 权限控制
  • 日志记录
  • 数据获取
  • 样式增强

按功能组织

src/
├── components/          # 可复用的 UI 组件
│   ├── Button/
│   │   ├── Button.js
│   │   ├── Button.css
│   │   └── Button.test.js
│   └── Card/
├── features/            # 功能模块
│   ├── User/
│   │   ├── UserList.js
│   │   ├── UserForm.js
│   │   └── userSlice.js
│   └── Product/
├── hooks/               # 自定义 Hooks
├── utils/               # 工具函数
├── api/                 # API 调用
└── App.js

按类型组织

src/
├── components/          # 所有组件
├── containers/          # 容器组件
├── actions/             # Redux actions
├── reducers/            # Redux reducers
├── utils/               # 工具函数
└── App.js

本章介绍了组件的基础知识:

  • 函数组件和类组件
  • 组件组合
  • Props 传递和使用
  • 默认 Props 和类型检查
  • children 和 render props
  • 高阶组件

组件是 React 的核心概念,理解组件的各种用法是掌握 React 的关键。

Hooks 是 React 16.8 引入的新特性,它让我们在函数组件中使用 state 和其他 React 特性,而无需编写类组件。

为什么需要 Hooks

  • 类组件中的 this 指向难以理解
  • 生命周期方法中常常包含不相关的逻辑,而相关逻辑分散在不同生命周期中
  • 高阶组件和 render props 会导致组件嵌套地狱
  • 复用状态逻辑困难

Hooks 使用规则

  • 只在最顶层调用 Hooks,不要在循环、条件或嵌套函数中调用
  • 只在 React 函数组件或自定义 Hooks 中调用 Hooks

useState 是最常用的 Hook,用于在函数组件中添加状态。

基本用法

import { useState } from 'react';
 
function Counter() {
  // 声明一个叫 "count" 的 state 变量,初始值为 0
  const [count, setCount] = useState(0);
 
  return (
    <div>
      <p>你点击了 {count}</p>
      <button onClick={() => setCount(count + 1)}>
        点击我
      </button>
    </div>
  );
}

解构赋值说明

`const [count, setCount] = useState(0)` 是数组解构: - count: 当前状态值 - setCount: 更新状态的函数

函数式更新

当新状态依赖于旧状态时,使用函数式更新可以避免闭包问题。

function Counter() {
  const [count, setCount] = useState(0);
 
  // 正确:使用函数式更新
  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };
 
  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>增加</button>
    </div>
  );
}

对象状态

当 state 是对象时,更新时需要展开旧值。

function UserForm() {
  const [user, setUser] = useState({
    name: '',
    age: 0,
    email: ''
  });
 
  const handleChange = (field, value) => {
    setUser(prevUser => ({
      ...prevUser,  // 展开旧值
      [field]: value // 更新特定字段
    }));
  };
 
  return (
    <div>
      <input 
        value={user.name} 
        onChange={e => handleChange('name', e.target.value)}
        placeholder="姓名"
      />
    </div>
  );
}

延迟初始化

当初始值计算开销较大时,可以传入函数进行延迟初始化。

function ExpensiveComponent() {
  // 只会在首次渲染时执行 computeExpensiveValue
  const [value, setValue] = useState(() => computeExpensiveValue());
 
  // ...
}

useEffect 是 React Hooks 中最重要、最基础的 Hook 之一,用于在函数组件中执行副作用操作。

基本语法

import { useEffect } from 'react';
 
function Example() {
  useEffect(() => {
    // 副作用逻辑
    console.log('组件挂载或更新');
 
    // 可选的清理函数
    return () => {
      console.log('清理工作');
    };
  }, [/* 依赖数组 */]);
 
  return <div>示例组件</div>;
}

执行时机

// 1. 每次渲染后都执行
useEffect(() => {
  console.log('每次渲染都会执行');
});
 
// 2. 只在挂载时执行(空依赖数组)
useEffect(() => {
  console.log('只在挂载时执行');
  fetchData();
}, []);
 
// 3. 依赖变化时执行
useEffect(() => {
  console.log(`count 变化为: ${count}`);
  document.title = `点击了 ${count} 次`;
}, [count]);

清理函数(Cleanup)

function ChatRoom({ roomId }) {
  useEffect(() => {
    // 建立连接
    const connection = createConnection(roomId);
    connection.connect();
    console.log(`连接到房间: ${roomId}`);
 
    // 返回清理函数
    return () => {
      connection.disconnect();
      console.log(`断开房间: ${roomId} 的连接`);
    };
  }, [roomId]);
 
  return <div>聊天房间: {roomId}</div>;
}

数据获取模式

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    let isCancelled = false;
    const controller = new AbortController();
 
    async function fetchUser() {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`, {
          signal: controller.signal
        });
        const data = await response.json();
 
        if (!isCancelled) {
          setUser(data);
          setError(null);
        }
      } catch (err) {
        if (!isCancelled && err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        if (!isCancelled) {
          setLoading(false);
        }
      }
    }
 
    fetchUser();
 
    return () => {
      isCancelled = true;
      controller.abort();
    };
  }, [userId]);
 
  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;
 
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

useEffect 与 useLayoutEffect

import { useLayoutEffect } from 'react';
 
function MeasureExample() {
  const divRef = useRef(null);
  const [width, setWidth] = useState(0);
 
  useLayoutEffect(() => {
    // 在浏览器绘制前测量 DOM
    const { width } = divRef.current.getBoundingClientRect();
    setWidth(width);
  }, []);
 
  return (
    <div ref={divRef}>
      宽度: {width}px
    </div>
  );
}

- useEffect:在浏览器绘制完成后异步执行,不会阻塞视觉更新 - useLayoutEffect:在浏览器绘制之前同步执行,会阻塞视觉更新

绝大多数情况下应该使用 useEffect,只有在需要同步测量/修改 DOM 时才使用 useLayoutEffect。

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。这个对象在组件的整个生命周期内保持不变。

访问 DOM 元素

import { useRef } from 'react';
 
function TextInput() {
  const inputRef = useRef(null);
 
  const focusInput = () => {
    // 直接访问 DOM 元素
    inputRef.current.focus();
  };
 
  return (
    <div>
      <input ref={inputRef} type="text" placeholder="输入内容" />
      <button onClick={focusInput}>聚焦输入框</button>
    </div>
  );
}

保存上一次的值

function Counter() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef();
 
  useEffect(() => {
    // 在渲染后保存当前值,供下一次渲染使用
    prevCountRef.current = count;
  });
 
  const prevCount = prevCountRef.current;
 
  return (
    <div>
      <p>当前: {count}, 上一次: {prevCount}</p>
      <button onClick={() => setCount(c => c + 1)}>增加</button>
    </div>
  );
}

保存定时器 ID

function Timer() {
  const [seconds, setSeconds] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  const intervalRef = useRef(null);
 
  const start = () => {
    if (!isRunning) {
      setIsRunning(true);
      intervalRef.current = setInterval(() => {
        setSeconds(s => s + 1);
      }, 1000);
    }
  };
 
  const stop = () => {
    if (isRunning) {
      setIsRunning(false);
      clearInterval(intervalRef.current);
    }
  };
 
  const reset = () => {
    stop();
    setSeconds(0);
  };
 
  useEffect(() => {
    // 组件卸载时清理定时器
    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, []);
 
  return (
    <div>
      <p>{seconds}</p>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
      <button onClick={reset}>重置</button>
    </div>
  );
}

转发 ref(forwardRef)

import { forwardRef, useRef } from 'react';
 
// 子组件转发 ref 到内部 DOM 元素
const FancyInput = forwardRef((props, ref) => {
  return <input ref={ref} className="fancy-input" {...props} />;
});
 
// 父组件使用
function Parent() {
  const inputRef = useRef(null);
 
  const focusInput = () => {
    inputRef.current.focus();
  };
 
  return (
    <div>
      <FancyInput ref={inputRef} placeholder="输入内容" />
      <button onClick={focusInput}>聚焦</button>
    </div>
  );
}

useImperativeHandle 自定义暴露的实例值

import { forwardRef, useRef, useImperativeHandle } from 'react';
 
const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef(null);
 
  // 自定义暴露给父组件的方法
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    clear: () => {
      inputRef.current.value = '';
    },
    getValue: () => {
      return inputRef.current.value;
    }
  }));
 
  return <input ref={inputRef} {...props} />;
});
 
// 父组件使用
function Parent() {
  const fancyInputRef = useRef(null);
 
  const handleClick = () => {
    fancyInputRef.current.focus();
    console.log(fancyInputRef.current.getValue());
    fancyInputRef.current.clear();
  };
 
  return (
    <div>
      <FancyInput ref={fancyInputRef} />
      <button onClick={handleClick}>操作输入框</button>
    </div>
  );
}

useCallback 返回一个 memoized 回调函数。只有当依赖项发生变化时,才会返回新的函数。

基本用法

import { useCallback } from 'react';
 
function Parent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');
 
  // 有 useCallback:只在 count 变化时创建新函数
  const handleClick = useCallback(() => {
    console.log('count:', count);
  }, [count]);
 
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>增加</button>
      <input value={text} onChange={e => setText(e.target.value)} />
      <Child onClick={handleClick} />
    </div>
  );
}

配合 React.memo 使用

import { memo, useCallback, useState } from 'react';
 
// 子组件使用 React.memo 进行浅比较
const Child = memo(({ onClick, label }) => {
  console.log(`${label} 渲染`);
  return <button onClick={onClick}>{label}</button>;
});
 
function Parent() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);
 
  // 使用 useCallback 缓存函数
  const handleClick1 = useCallback(() => {
    setCount1(c => c + 1);
  }, []);
 
  const handleClick2 = useCallback(() => {
    setCount2(c => c + 1);
  }, []);
 
  return (
    <div>
      <p>Count1: {count1}, Count2: {count2}</p>
      <Child onClick={handleClick1} label="增加 Count1" />
      <Child onClick={handleClick2} label="增加 Count2" />
    </div>
  );
}

何时使用 useCallback

  • 函数作为 props 传递给子组件,且子组件使用了 React.memo
  • 函数作为 useEffect 的依赖项
  • 函数被其他 Hooks(如 useMemo)依赖
  • 函数是一个复杂计算或创建成本较高

避免过度使用

// 不需要 useCallback 的情况:简单函数直接内联
function SimpleComponent() {
  const [count, setCount] = useState(0);
 
  // 简单函数,不需要 useCallback
  const increment = () => setCount(c => c + 1);
 
  return <button onClick={increment}>{count}</button>;
}

useMemo 返回一个 memoized 值。只有当依赖项发生变化时,才会重新计算。

基本用法

import { useMemo } from 'react';
 
function ExpensiveComponent({ data, filter }) {
  // 使用 useMemo 缓存昂贵的计算结果
  const filteredData = useMemo(() => {
    console.log('计算过滤数据');
    return data.filter(item => item.name.includes(filter));
  }, [data, filter]);
 
  return (
    <ul>
      {filteredData.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

对象/数组的稳定性

function Chart({ data, options }) {
  // 使用 useMemo 保持对象的引用稳定
  const chartOptions = useMemo(() => ({
    responsive: true,
    scales: {
      y: { beginAtZero: options.startFromZero }
    }
  }), [options.startFromZero]);
 
  // chartOptions 的引用只在依赖变化时改变
  useEffect(() => {
    console.log('options 变化,重新初始化图表');
    initChart(chartOptions);
  }, [chartOptions]);
 
  return <canvas id="chart" />;
}

复杂数据的处理

function DataTable({ rows, sortKey, sortOrder }) {
  const sortedRows = useMemo(() => {
    return [...rows].sort((a, b) => {
      const aVal = a[sortKey];
      const bVal = b[sortKey];
 
      if (sortOrder === 'asc') {
        return aVal > bVal ? 1 : -1;
      } else {
        return aVal < bVal ? 1 : -1;
      }
    });
  }, [rows, sortKey, sortOrder]);
 
  return (
    <table>
      <tbody>
        {sortedRows.map(row => (
          <tr key={row.id}>
            <td>{row.name}</td>
            <td>{row.value}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

useMemo vs useCallback

// useCallback(fn, deps) 等同于 useMemo(() => fn, deps)
 
// useCallback - 缓存函数
const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);
 
// useMemo - 缓存值
const memoizedValue = useMemo(() => {
  return computeExpensiveValue(a, b);
}, [a, b]);

useContext 让我们无需为每层组件手动添加 props,就能在组件树间进行数据传递。

创建和使用 Context

import { createContext, useContext, useState } from 'react';
 
// 1. 创建 Context
const ThemeContext = createContext('light');
 
// 2. 提供 Context
function App() {
  const [theme, setTheme] = useState('light');
 
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}
 
// 3. 消费 Context
function Toolbar() {
  return (
    <div>
      <ThemeButton />
    </div>
  );
}
 
function ThemeButton() {
  // 使用 useContext 获取上下文值
  const { theme, setTheme } = useContext(ThemeContext);
 
  return (
    <button
      style={{ background: theme === 'dark' ? '#333' : '#fff' }}
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
    >
      当前主题: {theme}
    </button>
  );
}

多 Context 使用

const ThemeContext = createContext('light');
const UserContext = createContext(null);
 
function App() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState({ name: '张三' });
 
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <UserContext.Provider value={{ user, setUser }}>
        <Layout />
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}
 
function Layout() {
  const { theme } = useContext(ThemeContext);
  const { user } = useContext(UserContext);
 
  return (
    <div className={theme}>
      <h1>欢迎, {user.name}</h1>
    </div>
  );
}

Context 分割优化

// 不好的做法:整个 context 变化导致所有消费者重新渲染
const AppContext = createContext({
  theme: 'light',
  user: null,
  setTheme: () => {},
  setUser: () => {}
});
 
// 更好的做法:将经常变化和很少变化的数据分开
const ThemeContext = createContext({ theme: 'light', setTheme: () => {} });
const UserContext = createContext({ user: null, setUser: () => {} });
 
// 最佳做法:使用自定义 Hook 封装 context
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme 必须在 ThemeProvider 内部使用');
  }
  return context;
}

useReducer 是 useState 的替代方案,适用于 state 逻辑较复杂或包含多个子值的情况。

基本用法

import { useReducer } from 'react';
 
// 定义 reducer 函数
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    default:
      throw new Error('未知 action');
  }
}
 
function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
 
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>重置</button>
    </div>
  );
}

复杂状态管理

const initialState = {
  loading: false,
  data: null,
  error: null
};
 
function dataReducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null };
    case 'FETCH_SUCCESS':
      return { ...state, loading: false, data: action.payload };
    case 'FETCH_ERROR':
      return { ...state, loading: false, error: action.payload };
    default:
      return state;
  }
}
 
function DataComponent() {
  const [state, dispatch] = useReducer(dataReducer, initialState);
 
  const fetchData = async () => {
    dispatch({ type: 'FETCH_START' });
    try {
      const response = await fetch('/api/data');
      const data = await response.json();
      dispatch({ type: 'FETCH_SUCCESS', payload: data });
    } catch (error) {
      dispatch({ type: 'FETCH_ERROR', payload: error.message });
    }
  };
 
  return (
    <div>
      {state.loading && <p>加载中...</p>}
      {state.error && <p>错误: {state.error}</p>}
      {state.data && <p>数据: {JSON.stringify(state.data)}</p>}
      <button onClick={fetchData}>获取数据</button>
    </div>
  );
}

useReducer vs useState

场景 推荐方案
—————
简单状态(单个值) useState
复杂状态(多个子值相互依赖) useReducer
需要优化性能,state 更新逻辑复杂 useReducer
需要复用状态逻辑 useReducer + 自定义 Hook

useId - 生成唯一 ID

import { useId } from 'react';
 
function Form() {
  const id = useId();
 
  return (
    <div>
      <label htmlFor={id + '-name'}>姓名:</label>
      <input id={id + '-name'} type="text" />
 
      <label htmlFor={id + '-email'}>邮箱:</label>
      <input id={id + '-email'} type="email" />
    </div>
  );
}

useTransition - 非紧急更新

import { useTransition, useState } from 'react';
 
function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('home');
 
  const selectTab = (nextTab) => {
    // 将状态更新标记为低优先级
    startTransition(() => {
      setTab(nextTab);
    });
  };
 
  return (
    <div>
      {isPending && <p>加载中...</p>}
      <TabButton onClick={() => selectTab('home')}>首页</TabButton>
      <TabButton onClick={() => selectTab('about')}>关于</TabButton>
      {tab === 'home' && <HomeTab />}
      {tab === 'about' && <AboutTab />}
    </div>
  );
}

useDeferredValue - 延迟更新值

import { useDeferredValue, useState } from 'react';
 
function SearchResults() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
 
  return (
    <div>
      <input 
        value={query} 
        onChange={e => setQuery(e.target.value)}
        placeholder="搜索..."
      />
      {/* 使用 deferredQuery 显示结果,保持输入流畅 */}
      <SlowList query={deferredQuery} />
    </div>
  );
}

自定义 Hooks 是提取组件逻辑到可复用函数的方式。自定义 Hook 是一个函数,其名称以 “use” 开头,内部可以调用其他 Hooks。

useFetch - 数据获取

import { useState, useEffect } from 'react';
 
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    const controller = new AbortController();
 
    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) {
          throw new Error(`HTTP 错误: ${response.status}`);
        }
        const result = await response.json();
        setData(result);
        setError(null);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
          setData(null);
        }
      } finally {
        setLoading(false);
      }
    }
 
    fetchData();
 
    return () => controller.abort();
  }, [url]);
 
  return { data, loading, error };
}
 
// 使用
function UserList() {
  const { data: users, loading, error } = useFetch('/api/users');
 
  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;
 
  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

useLocalStorage - 本地存储同步

import { useState, useEffect } from 'react';
 
function useLocalStorage(key, initialValue) {
  // 获取初始值
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });
 
  // 当值变化时更新 localStorage
  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(storedValue));
    } catch (error) {
      console.error(error);
    }
  }, [key, storedValue]);
 
  return [storedValue, setStoredValue];
}
 
// 使用
function App() {
  const [name, setName] = useLocalStorage('name', '');
 
  return (
    <input 
      value={name} 
      onChange={e => setName(e.target.value)}
      placeholder="输入你的名字"
    />
  );
}

useDebounce - 防抖

import { useState, useEffect } from 'react';
 
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
 
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
 
    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);
 
  return debouncedValue;
}
 
// 使用
function SearchInput() {
  const [input, setInput] = useState('');
  const debouncedInput = useDebounce(input, 500);
 
  useEffect(() => {
    if (debouncedInput) {
      performSearch(debouncedInput);
    }
  }, [debouncedInput]);
 
  return (
    <input
      value={input}
      onChange={(e) => setInput(e.target.value)}
      placeholder="搜索..."
    />
  );
}

usePrevious - 获取上一次的值

import { useRef, useEffect } from 'react';
 
function usePrevious(value) {
  const ref = useRef();
 
  useEffect(() => {
    ref.current = value;
  });
 
  return ref.current;
}
 
// 使用
function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
 
  return (
    <div>
      <p>当前: {count}, 上一次: {prevCount}</p>
      <button onClick={() => setCount(c => c + 1)}>增加</button>
    </div>
  );
}

useOnClickOutside - 点击外部关闭

import { useEffect, useRef } from 'react';
 
function useOnClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      // 如果点击的是 ref 元素内部,不执行 handler
      if (!ref.current || ref.current.contains(event.target)) {
        return;
      }
      handler(event);
    };
 
    document.addEventListener('mousedown', listener);
    document.addEventListener('touchstart', listener);
 
    return () => {
      document.removeEventListener('mousedown', listener);
      document.removeEventListener('touchstart', listener);
    };
  }, [ref, handler]);
}
 
// 使用
function Dropdown() {
  const ref = useRef();
  const [isOpen, setIsOpen] = useState(false);
 
  useOnClickOutside(ref, () => setIsOpen(false));
 
  return (
    <div ref={ref}>
      <button onClick={() => setIsOpen(!isOpen)}>切换</button>
      {isOpen && <div className="dropdown">下拉内容</div>}
    </div>
  );
}

1. 只在最顶层调用 Hooks

// 正确
function Example() {
  const [count, setCount] = useState(0);
  useEffect(() => { });
 
  if (condition) {  // 条件在 Hooks 之后
    return;
  }
}
 
// 错误 - 条件中使用 Hook
function Example() {
  if (condition) {
    const [count, setCount] = useState(0);  // 错误!
  }
}

2. 只在 React 函数中调用 Hooks

// 正确 - 在函数组件中
function MyComponent() {
  const [state, setState] = useState(0);
}
 
// 正确 - 在自定义 Hook 中
function useMyHook() {
  const [state, setState] = useState(0);
}

3. 使用 ESLint 插件

{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

4. 分离不相关的副作用

// 好的做法:分离关注点
function Example() {
  // 处理订阅
  useEffect(() => {
    const subscription = subscribe();
    return () => subscription.unsubscribe();
  }, []);
 
  // 处理 DOM 操作
  useEffect(() => {
    document.title = '新标题';
  }, []);
}

5. 避免过度优化

// 不需要 useMemo 的简单计算
const doubled = count * 2;
 
// 不需要 useCallback 的简单函数
const handleClick = () => setCount(c => c + 1);

陷阱 1:useEffect 无限循环

// 错误:setState 导致重新渲染,形成无限循环
useEffect(() => {
  setCount(count + 1);
}, [count]);
 
// 正确:使用函数式更新
useEffect(() => {
  const timer = setTimeout(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearTimeout(timer);
}, []);

陷阱 2:遗漏依赖

// 错误:遗漏了 count 依赖
useEffect(() => {
  console.log(`当前计数: ${count}`);
}, []); // 警告:count 应该在依赖数组中
 
// 正确:添加所有依赖
useEffect(() => {
  console.log(`当前计数: ${count}`);
}, [count]);

陷阱 3:对象和数组依赖

// 问题:options 每次都是新对象,导致 effect 每次都执行
useEffect(() => {
  fetchData(options);
}, [options]);
 
// 方案1:展开为原始值
useEffect(() => {
  fetchData({ page, size });
}, [page, size]);
 
// 方案2:使用 useMemo
const options = useMemo(() => ({ page, size }), [page, size]);

本章全面介绍了 React 组件和 Hooks:

组件基础

  • 函数组件和类组件的定义与区别
  • Props 传递、默认值和类型检查
  • 组件组合、children 和 render props
  • 高阶组件和组件组织方式

核心 Hooks

  • useState:状态管理
  • useEffect:副作用处理
  • useRef:DOM 引用和持久化值
  • useCallback:缓存回调函数
  • useMemo:缓存计算结果
  • useContext:跨组件状态共享
  • useReducer:复杂状态管理

其他 Hooks

  • useId:生成唯一 ID
  • useTransition:非紧急更新
  • useDeferredValue:延迟更新值

自定义 Hooks

  • 封装可复用逻辑
  • 常见的自定义 Hooks 模式

最佳实践

  • Hooks 使用规则
  • 性能优化策略
  • 常见陷阱与解决方案

掌握这些知识后,你可以使用函数组件和 Hooks 构建高质量的 React 应用。记住 Hooks 的核心原则:只在最顶层调用、只在 React 函数中调用、保持依赖数组完整。

该主题尚不存在

您访问的页面并不存在。如果允许,您可以使用创建该页面按钮来创建它。

  • react/组件基础.1775530957.txt.gz
  • 最后更改: 2026/04/07 11:02
  • 张叶安