普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月17日首页

react组件(3)---组件间的通信

作者 前端无涯
2025年12月17日 12:25

引言

在React应用开发中,组件是构建用户界面的基本单元。随着应用复杂度增加,多个组件之间的通信变得至关重要。无论是简单的父子组件通信,还是复杂的跨组件数据流,选择合适的通信方式将直接影响代码的可维护性和性能。本文将全面介绍React中组件通信的各种方法,帮助你在不同场景下做出最佳选择。

1. 父子组件通信

1.1 父组件向子组件传递数据:Props

最基本的通信方式是父组件通过props向子组件传递数据。这是React单向数据流的核心体现。

// 父组件
function ParentComponent() {
  const userData = { name: '张三', age: 28 };
  
  return (
    <div>
      <ChildComponent user={userData} title="用户信息" />
    </div>
  );
}

// 子组件
function ChildComponent({ user, title }) {
  return (
    <div>
      <h3>{title}</h3>
      <p>姓名: {user.name}</p>
      <p>年龄: {user.age}</p>
    </div>
  );
}

1.2 子组件向父组件传递数据:回调函数

子组件通过调用父组件传递的回调函数,实现向父组件传递数据。

// 父组件
function ParentComponent() {
  const [message, setMessage] = useState('');
  
  const handleDataFromChild = (data) => {
    setMessage(data);
    console.log('来自子组件的数据:', data);
  };
  
  return (
    <div>
      <p>父组件接收到的消息: {message}</p>
      <ChildComponent onSendData={handleDataFromChild} />
    </div>
  );
}

// 子组件
function ChildComponent({ onSendData }) {
  const handleClick = () => {
    onSendData('Hello from Child!');
  };
  
  return (
    <button onClick={handleClick}>向父组件发送数据</button>
  );
}

2. 兄弟组件通信

兄弟组件之间的通信需要通过它们的共同父组件作为中介。

function ParentComponent() {
  const [sharedData, setSharedData] = useState('');
  
  // 处理来自第一个子组件的数据
  const handleDataFromA = (data) => {
    setSharedData(data);
  };
  
  return (
    <div>
      <ChildA onDataUpdate={handleDataFromA} />
      <ChildB receivedData={sharedData} />
    </div>
  );
}

function ChildA({ onDataUpdate }) {
  const sendData = () => {
    onDataUpdate('数据来自ChildA');
  };
  
  return <button onClick={sendData}>发送数据给兄弟组件</button>;
}

function ChildB({ receivedData }) {
  return <div>ChildB接收到的数据: {receivedData}</div>;
}

这种方式的优点是数据流清晰可见,但当组件层级较深时,可能会导致"prop drilling"问题。

Prop Drilling(直译 “属性钻取”,也常被称为 “Prop 透传”)指的是:当一个数据需要从父组件传递到深层嵌套的子组件(如祖父→父亲→儿子→孙子)时,中间的每一层组件都需要接收并传递这个属性,即使中间组件本身并不需要使用该属性

3. 跨层级组件通信

3.1 Context API

对于深层嵌套的组件通信,React的Context API提供了一种在组件树中传递数据的方法,而无需显式地通过每个层级传递props。

// 1. 创建Context
const ThemeContext = React.createContext();

// 2. 提供数据(Provider)
function App() {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Header />
      <MainContent />
    </ThemeContext.Provider>
  );
}

// 3. 在任意层级消费数据(Consumer)
function Header() {
  return (
    <header>
      <ThemeToggle />
    </header>
  );
}

function ThemeToggle() {
  const { theme, setTheme } = useContext(ThemeContext);
  
  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light');
  };
  
  return (
    <button onClick={toggleTheme}>
      当前主题: {theme} (点击切换)
    </button>
  );
}

Context API特别适合全局数据,如主题、用户认证信息、语言偏好等。

3.2 组合模式(Component Composition)

通过组合组件的方式,可以避免不必要的层级嵌套。

// 不推荐:深层prop传递
function App() {
  const user = { name: 'John' };
  return <Header user={user} />;
}

function Header({ user }) {
  return <Navigation user={user} />;
}

function Navigation({ user }) {
  return <UserMenu user={user} />;
}

// 推荐:组件组合
function App() {
  const user = { name: 'John' };
  return (
    <Header>
      <Navigation>
        <UserMenu user={user} />
      </Navigation>
    </Header>
  );
}

4. 全局状态管理

4.1 使用Redux

对于复杂应用,Redux提供了可预测的状态管理。

// store.js
import { createStore } from 'redux';

const initialState = { count: 0 };

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
}

const store = createStore(counterReducer);

// Component.js
import { useSelector, useDispatch } from 'react-redux';

function Counter() {
  const count = useSelector(state => state.count);
  const dispatch = useDispatch();
  
  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
    </div>
  );
}

4.2 使用useReducer + Context

对于中等复杂度应用,可以使用useReducer配合Context实现类Redux的状态管理。

const AppStateContext = React.createContext();

function appReducer(state, action) {
  switch (action.type) {
    case 'SET_USER':
      return { ...state, user: action.payload };
    case 'SET_THEME':
      return { ...state, theme: action.payload };
    default:
      return state;
  }
}

function AppProvider({ children }) {
  const [state, dispatch] = useReducer(appReducer, {
    user: null,
    theme: 'light'
  });
  
  return (
    <AppStateContext.Provider value={{ state, dispatch }}>
      {children}
    </AppStateContext.Provider>
  );
}

// 在任何组件中使用
function UserProfile() {
  const { state, dispatch } = useContext(AppStateContext);
  
  const updateUser = (user) => {
    dispatch({ type: 'SET_USER', payload: user });
  };
  
  return <div>用户名: {state.user?.name}</div>;
}

5. 其他通信方式

5.1 事件总线(Event Bus)

对于非父子关系且层级较远的组件,可以使用事件总线实现通信。

// eventBus.js
class EventBus {
  constructor() {
    this.events = {};
  }
  
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  }
  
  emit(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(data));
    }
  }
}

export default new EventBus();

// 组件A - 发布事件
function ComponentA() {
  const handleClick = () => {
    eventBus.emit('dataUpdate', { message: 'Hello from A' });
  };
  
  return <button onClick={handleClick}>发布事件</button>;
}

// 组件B - 订阅事件
function ComponentB() {
  const [data, setData] = useState('');
  
  useEffect(() => {
    eventBus.on('dataUpdate', (newData) => {
      setData(newData.message);
    });
  }, []);
  
  return <div>接收到的数据: {data}</div>;
}

5.2 Ref方式通信

父组件通过ref直接调用子组件的方法。

// 子组件
class ChildComponent extends React.Component {
  doSomething() {
    console.log('子组件方法被调用');
  }
  
  render() {
    return <div>子组件</div>;
  }
}

// 父组件
function ParentComponent() {
  const childRef = useRef();
  
  const handleClick = () => {
    childRef.current.doSomething();
  };
  
  return (
    <div>
      <button onClick={handleClick}>调用子组件方法</button>
      <ChildComponent ref={childRef} />
    </div>
  );
}

6. 通信方式选择指南

以下表格总结了不同场景下推荐的通信方式:

通信场景 推荐方式 优点 缺点
父子组件 Props + 回调函数 简单直观,数据流清晰 深层嵌套时繁琐
兄弟组件 共同父组件中转 易于理解 可能导致父组件臃肿
跨层级组件 Context API 避免prop drilling 可能引起不必要的重渲染
全局状态 Redux/Zustand 可预测,调试友好 概念复杂,样板代码多
解耦通信 事件总线/发布订阅 组件完全解耦 数据流不够明显

7. 最佳实践与注意事项

  1. 保持状态提升合理:将状态提升到足够高的层级,但不要过度提升。
  2. 避免过度使用Context:Context的变动会引起所有消费者组件重新渲染,性能敏感场景需谨慎。
  3. 不可变更新状态:始终以不可变方式更新状态,避免直接修改对象或数组。
  4. TypeScript集成:使用TypeScript定义props和context的类型,提高代码可靠性。
  5. 性能优化:使用React.memo、useMemo、useCallback避免不必要的重渲染。

结语

React组件通信是应用开发的核心环节,选择正确的通信方式至关重要。简单场景优先考虑props和回调函数,复杂场景再考虑Context或状态管理库。记住,没有一种方法适合所有场景,关键是理解每种方法的优缺点,根据具体需求做出合理选择。

希望本文能帮助你在React开发中更加游刃有余地处理组件通信问题。Happy Coding!

react组件(2)---State 与生命周期

作者 前端无涯
2025年12月17日 12:15

1. 什么是 React State?

State(状态)是 React 组件中存储可变数据的容器,它决定了组件的行为和渲染输出。与 Props 不同,State 是组件内部管理且可以变化的,而 Props 是从父组件传递过来的只读属性。

State 的核心特征包括:

  • 可变性:State 可以在组件生命周期内发生变化
  • 响应式:State 的改变会自动触发组件的重新渲染
  • 局部性:State 是组件私有的,其他组件无法直接访问
  • 异步性:setState 操作可能是异步的,React 会批量处理状态更新

在 React 中,将组件视为一个状态机,通过管理状态的变化来驱动 UI 的更新。

2. 类组件中的 State

2.1 状态的声明与初始化

在类组件中,State 通常在构造函数中初始化:

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      isActive: false
    };
  }
}

现代写法也可以使用类属性语法:

class Counter extends React.Component {
  state = {
    count: 0,
    isActive: false
  };
}

2.2 状态的更新

更新 State 必须使用 setState()方法,绝对不能直接修改 state

// 错误做法
this.state.count = 1; // 不会触发重新渲染!

// 正确做法
this.setState({ count: 1 });

// 当新状态依赖于旧状态时,使用函数形式
this.setState(prevState => ({
  count: prevState.count + 1
}));

3. 函数组件中的 State(useState Hook)

React 16.8 引入了 Hooks,允许函数组件使用 State。useState是最基础的 Hook。

3.1 useState 的基本用法

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>当前计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>
        点击增加
      </button>
    </div>
  );
}

3.2 useState 的高级用法

函数式更新(当新状态依赖于旧状态时):

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

// 推荐:使用函数式更新确保基于最新状态
const increment = () => {
  setCount(prevCount => prevCount + 1);
};

延迟初始化(当初始状态需要复杂计算时):

const [state, setState] = useState(() => {
  const expensiveValue = performExpensiveCalculation();
  return expensiveValue;
});

对象和数组的状态管理

// 对象状态更新
const [user, setUser] = useState({ name: 'Alice', age: 25 });
setUser(prevUser => ({ ...prevUser, name: 'Bob' }));

// 数组状态更新
const [items, setItems] = useState(['apple', 'banana']);
setItems(prevItems => [...prevItems, 'orange']);

4. React 组件生命周期

类组件生命周期 函数组件 useEffect 实现方式
componentDidMount useEffect (() => {}, [])(空依赖数组)
componentDidUpdate useEffect (() => {}, [deps])(依赖项变化时执行)
componentWillUnmount useEffect (() => { return () => {} }, [])(返回清理函数)

4.1 生命周期概述

React 组件的生命周期可以分为三个主要阶段:

  • 挂载阶段:组件被创建并插入 DOM
  • 更新阶段:组件的 props 或 state 发生变化时重新渲染
  • 卸载阶段:组件从 DOM 中移除

4.2 类组件的生命周期方法

挂载阶段

  • constructor():初始化 state 和绑定方法
  • static getDerivedStateFromProps():在渲染前根据 props 更新 state
  • render():渲染组件(必需方法)
  • componentDidMount():组件挂载后执行,适合进行数据获取、订阅等副作用操作

更新阶段

  • static getDerivedStateFromProps():更新前根据 props 调整 state
  • shouldComponentUpdate():决定组件是否应该更新(性能优化关键)
  • render():重新渲染组件
  • getSnapshotBeforeUpdate():在 DOM 更新前捕获一些信息
  • componentDidUpdate():组件更新后执行,可以操作 DOM 或执行网络请求

卸载阶段

  • componentWillUnmount():组件卸载前执行清理操作,如取消定时器、网络请求等

4.3 函数组件中的"生命周期"(useEffect Hook)

useEffectHook 在函数组件中承担了生命周期方法的职责:

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

function Example() {
  const [count, setCount] = useState(0);
  
  // 相当于 componentDidMount + componentDidUpdate
  useEffect(() => {
    document.title = `点击了 ${count} 次`;
  });
  
  // 只在挂载时执行(类似 componentDidMount)
  useEffect(() => {
    console.log('组件挂载完成');
  }, []);
  
  // 依赖变化时执行(类似 componentDidUpdate)
  useEffect(() => {
    console.log(`count 变为: ${count}`);
  }, [count]);
  
  // 清理效果(类似 componentWillUnmount)
  useEffect(() => {
    const timer = setInterval(() => {
      // 一些操作
    }, 1000);
    
    return () => {
      clearInterval(timer); // 清理函数
    };
  }, []);
  
  return (
    <div>
      <p>你点击了 {count} 次</p>
      <button onClick={() => setCount(count + 1)}>
        点击我
      </button>
    </div>
  );
}

5. State 设计的最佳实践

5.1 State 的最小化原则

只将真正需要响应式更新的数据放入 State,派生数据可以在渲染时计算:

// 不好的做法:将派生数据放入 State
state = {
  items: [],
  totalCount: 0, // 可以从 items.length 派生
  filteredItems: [] // 可以从 items 过滤得到
};

// 好的做法:只存储原始数据
state = {
  items: []
};

// 派生数据在 render 中计算
get totalCount() {
  return this.state.items.length;
}

5.2 State 结构的扁平化

避免嵌套过深的 State 结构:

// 不好的嵌套结构
state = {
  user: {
    profile: {
      personalInfo: {
        name: '',
        age: 0
      }
    }
  }
};

// 好的扁平结构
state = {
  userName: '',
  userAge: 0
};

5.3 不可变更新模式

始终使用不可变的方式更新 State:

// 数组更新
// 错误:直接修改原数组
this.state.items.push(newItem);
// 正确:创建新数组
this.setState({
  items: [...this.state.items, newItem]
});

// 对象更新
// 错误:直接修改原对象
this.state.user.name = 'New Name';
// 正确:创建新对象
this.setState({
  user: { ...this.state.user, name: 'New Name' }
});

6. 常见场景与实战示例

6.1 数据获取场景

class DataFetcher extends React.Component {
  state = {
    data: [],
    loading: true,
    error: null,
    page: 1
  };
  
  componentDidMount() {
    this.fetchData();
  }
  
  componentDidUpdate(prevProps, prevState) {
    if (prevState.page !== this.state.page) {
      this.fetchData();
    }
  }
  
  fetchData = async () => {
    try {
      this.setState({ loading: true, error: null });
      const response = await fetch(`/api/data?page=${this.state.page}`);
      const result = await response.json();
      this.setState({ data: result, loading: false });
    } catch (error) {
      this.setState({ error: error.message, loading: false });
    }
  };
  
  render() {
    const { data, loading, error, page } = this.state;
    
    if (loading) return <div>加载中...</div>;
    if (error) return <div>错误: {error}</div>;
    
    return (
      <div>
        {/* 渲染数据 */}
      </div>
    );
  }
}

6.2 表单处理场景

function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });
  
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    // 提交表单数据
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        name="name"
        value={formData.name}
        onChange={handleChange}
      />
      <input
        name="email"
        value={formData.email}
        onChange={handleChange}
      />
      <textarea
        name="message"
        value={formData.message}
        onChange={handleChange}
      />
      <button type="submit">提交</button>
    </form>
  );
}

7. 常见陷阱与解决方案

7.1 过时状态问题

在闭包中捕获过时状态值:

// 问题代码:可能捕获过时状态
const [count, setCount] = useState(0);
const increment = () => {
  setTimeout(() => {
    setCount(count + 1); // 可能使用过时的 count 值
  }, 3000);
};

// 解决方案:使用函数式更新
const increment = () => {
  setTimeout(() => {
    setCount(prevCount => prevCount + 1); // 总是基于最新状态
  }, 3000);
};

7.2 useEffect 的依赖数组

正确处理 useEffect 的依赖数组避免无限循环:

// 错误:缺少依赖可能导致过时数据
useEffect(() => {
  fetchData(userId);
}, []);

// 错误:依赖不完整可能导致意外行为
useEffect(() => {
  fetchData(userId);
}, [userId]); // 如果 fetchData 使用了其他状态,需要包含

// 正确:包含所有依赖
useEffect(() => {
  fetchData(userId);
}, [userId, fetchData]); // 如果 fetchData 在渲染中定义,需要用 useCallback 包装

8. 总结

React 的 State 和生命周期是构建交互式界面的核心概念。无论是类组件还是函数组件,合理管理状态和理解组件生命周期都是开发高质量 React 应用的关键。

主要要点回顾

  • State 是组件内部的可变数据,Props 是从外部传入的只读数据
  • 类组件使用 this.statethis.setState(),函数组件使用 useStateHook
  • 生命周期方法让你在组件不同阶段执行代码,useEffect在函数组件中承担类似职责
  • 遵循 State 设计最佳实践(最小化、扁平化、不可变更新)
  • 注意常见陷阱,如过时状态和 useEffect 的依赖处理

随着 React 的发展,函数组件和 Hooks 已成为主流,但理解类组件的生命周期对于维护现有项目和深入理解 React 原理仍然很有价值。

希望本篇博客能帮助你更好地理解和应用 React 的 State 与生命周期概念!

react组件(1)---从入门到上手

作者 前端无涯
2025年12月17日 12:03

  组件是 React 的核心基石,也是 React 生态中最具代表性的设计思想。它将 UI 拆分为独立、可复用的单元,就像乐高积木一样,通过组合不同的组件可以构建出复杂的页面。从早期的类组件到如今的函数组件 + Hooks,React 组件的开发模式不断进化,变得更加简洁、灵活。本文将从组件本质、分类、创建、核心特性、通信、复用、性能优化等维度,全面解析 React 组件的使用方法与最佳实践,帮助你彻底掌握这一核心技能。

一、React 组件的本质:什么是组件?

1. 组件的定义

React 组件是独立的、可复用的 UI 单元,它接收输入(Props),处理内部逻辑(State/Hooks),最终返回用于描述 UI 的 JSX(或 React 元素)。简单来说,组件就是一个 “函数”:输入数据,输出 UI。

2. 组件的核心思想:模块化与复用

在传统的前端开发中,我们通常按页面划分代码,代码耦合度高,复用性差。而 React 的组件化思想解决了这一问题:

  • 单一职责:一个组件只负责完成一个特定的功能(比如一个按钮、一个列表、一个弹窗);
  • 可复用:组件可以在不同页面、不同项目中重复使用;
  • 可组合:组件可以嵌套、组合,形成更复杂的组件或页面;
  • 可维护:组件独立存在,修改一个组件不会影响其他组件,便于后期维护。

3. 组件的本质:函数或类

无论哪种类型的 React 组件,其本质都是JavaScript 函数或类

  • 函数组件:本质是一个普通的 JavaScript 函数,接收 props 参数,返回 JSX;
  • 类组件:本质是继承自React.Component的类,通过render方法返回 JSX。

举个最简单的例子,一个显示 “Hello React” 的组件:

// 函数组件(主流)
const Hello = () => {
  return <h1>Hello React</h1>;
};

// 类组件(旧版写法,现在极少使用)
import React from 'react';
class HelloClass extends React.Component {
  render() {
    return <h1>Hello React</h1>;
  }
}

二、React 组件的分类:不同类型的组件及适用场景

React 组件可以根据实现方式、职责、特性分为不同类型,了解这些分类有助于我们在开发中选择合适的组件类型。

1. 按实现方式划分:函数组件 vs 类组件

这是最核心的分类方式,也是 React 发展的重要分水岭。

特性 函数组件(Function Component) 类组件(Class Component)
语法复杂度 简洁,普通 JS 函数 繁琐,需要继承 React.Component,写 render 方法
状态管理 依赖 Hooks(useState、useReducer) 依赖 this.state 和 this.setState
生命周期 依赖 Hooks(useEffect) 有固定的生命周期方法(componentDidMount 等)
性能 略优(无类实例化开销) 略差(需要实例化类)
适用场景 所有场景(React 16.8 + 推荐) 老项目维护、特殊场景(如需要继承的组件)

注意:React 16.8 版本引入 Hooks 后,函数组件已经能够实现类组件的所有功能,目前函数组件 + Hooks是 React 开发的主流方式,类组件仅在老项目中存在。

2. 按职责划分:展示组件 vs 容器组件

这是一种基于 “关注点分离” 的分类方式,用于区分组件的功能职责。

展示组件(Presentational Component)

  • 职责:只负责 UI 的展示,不处理业务逻辑,也不管理状态;
  • 数据来源:通过 Props 接收外部传入的数据和回调函数;
  • 特点:纯函数式、可复用性高、无副作用。
// 展示组件:只渲染列表项,不处理数据逻辑
const TodoItem = ({ text, onDelete }) => {
  return (
    <li>
      {text}
      <button onClick={onDelete}>删除</button>
    </li>
  );
};

容器组件(Container Component)

  • 职责:负责处理业务逻辑、管理状态、请求数据,不关心 UI 展示;
  • 数据来源:自身的 State 或外部状态管理库(Redux、Mobx);
  • 特点:通常不直接渲染 JSX,而是将数据和方法通过 Props 传递给展示组件。
// 容器组件:处理待办事项的逻辑,管理状态
import { useState } from 'react';
const TodoListContainer = () => {
  const [todos, setTodos] = useState(['学习React组件', '写博客']);

  const handleDelete = (index) => {
    setTodos(todos.filter((_, i) => i !== index));
  };

  return (
    <ul>
      {todos.map((todo, index) => (
        <TodoItem key={index} text={todo} onDelete={() => handleDelete(index)} />
      ))}
    </ul>
  );
};

补充:随着 Hooks 的普及,这种分类方式逐渐淡化,因为我们可以通过自定义 Hooks 将业务逻辑抽离,让组件同时兼具展示和逻辑功能。

3. 其他常见分类

  • 纯组件(Pure Component) :类组件中的React.PureComponent或函数组件中的React.memo,用于优化性能,避免不必要的重渲染;
  • 高阶组件(Higher-Order Component,HOC) :一个接收组件并返回新组件的函数,用于复用组件逻辑;
  • 门户组件(Portal) :用于将组件渲染到 DOM 树的其他位置,如弹窗、提示框;
  • 懒加载组件:通过React.lazySuspense实现的按需加载组件。

三、组件的创建与基础使用

本节将重点讲解函数组件的创建与使用(类组件仅作简单介绍),包括 Props 接收、默认值、类型校验等核心知识点。

1. 基础函数组件的创建与渲染

(1)创建函数组件

函数组件是一个普通的 JavaScript 函数,返回值为 JSX(或 null、false,表示不渲染任何内容)。

// 无参数的基础组件
const Button = () => {
  return <button>点击我</button>;
};

// 箭头函数简写(无大括号时,return可省略)
const Text = () => <p>这是一段文本</p>;

// 普通函数写法(兼容旧版JS)
function Title() {
  return <h1>这是标题</h1>;
}

(2)渲染组件

组件创建完成后,可以像使用 HTML 标签一样在其他组件中渲染,注意组件名必须以大写字母开头(React 的约定,用于区分原生 HTML 标签)。

// 根组件
const App = () => {
  return (
    <div className="app">
      <Title />
      <Text />
      <Button />
    </div>
  );
};

// 渲染到DOM
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);

2. Props:组件的输入参数

Props(Properties 的缩写)是组件的输入参数,用于从父组件向子组件传递数据。Props 是只读的,子组件不能修改 Props(这是 React 的单向数据流原则)。

(1)接收与使用 Props

// 子组件:接收props
const Greeting = (props) => {
  // props是一个对象,包含父组件传递的所有属性
  return <h1>Hello, {props.name}!</h1>;
};

// 父组件:传递props
const App = () => {
  return (
    <div>
      {/* 传递字符串属性 */}
      <Greeting name="React" />
      {/* 传递非字符串属性(需用{}包裹) */}
      <Greeting age={18} />
      {/* 传递布尔值 */}
      <Greeting isShow={true} />
      {/* 传递函数 */}
      <Greeting onButtonClick={() => alert('点击了')} />
      {/* 传递JSX元素(对应props.children) */}
      <Greeting>
        <p>这是子元素</p>
      </Greeting>
    </div>
  );
};

(2)Props 解构赋值

为了简化代码,通常使用解构赋值直接提取 Props 中的属性。

// 基础解构
const Greeting = ({ name, age }) => {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>年龄:{age}</p>
    </div>
  );
};

// 解构+默认值
const Greeting = ({ name = '游客', age = 0 }) => {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>年龄:{age}</p>
    </div>
  );
};

(3)默认 Props

除了使用解构赋值设置默认值,还可以通过defaultProps属性设置组件的默认 Props(适用于函数组件和类组件)。

const Greeting = ({ name, age }) => {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>年龄:{age}</p>
    </div>
  );
};

// 设置默认Props
Greeting.defaultProps = {
  name: '游客',
  age: 0
};

(4)Props 类型校验

为了提高代码的健壮性,我们可以使用prop-types库对 Props 的类型进行校验(React v15.5.0 后,PropTypes 从 React 核心库中移出,需单独安装)。

步骤 1:安装 prop-types

npm install prop-types --save
# 或
yarn add prop-types

步骤 2:使用 PropTypes 进行类型校验

import PropTypes from 'prop-types';

const Greeting = ({ name, age, isShow, onButtonClick, children }) => {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>年龄:{age}</p>
      {children}
    </div>
  );
};

// 类型校验
Greeting.propTypes = {
  // 字符串,必传
  name: PropTypes.string.isRequired,
  // 数字(也可以是字符串)
  age: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  // 布尔值
  isShow: PropTypes.bool,
  // 函数
  onButtonClick: PropTypes.func,
  // 任意React节点
  children: PropTypes.node
};

// 默认Props
Greeting.defaultProps = {
  name: '游客',
  age: 0,
  isShow: false
};

3. 类组件的创建(仅作了解)

类组件需要继承React.Component,并实现render方法返回 JSX。

import React from 'react';
import PropTypes from 'prop-types';

class Greeting extends React.Component {
  // 默认Props
  static defaultProps = {
    name: '游客',
    age: 0
  };

  // 类型校验
  static propTypes = {
    name: PropTypes.string.isRequired,
    age: PropTypes.number
  };

  render() {
    const { name, age } = this.props;
    return (
      <div>
        <h1>Hello, {name}!</h1>
        <p>年龄:{age}</p>
      </div>
    );
  }
}

react---JSX完全指南:从基础语法到进阶实战

作者 前端无涯
2025年12月17日 11:19

JSX(JavaScript XML)是 React 生态中最具辨识度的特性之一,它将类 HTML 的语法嵌入 JavaScript 中,让开发者能够以直观的方式编写 UI 结构,同时保留 JavaScript 的逻辑能力。很多开发者最初会将 JSX 误认为是 “HTML 在 JS 中的变体”,但实际上它是 JavaScript 的语法糖,最终会被编译为普通的 JavaScript 函数调用。本文将从本质、基础语法、进阶用法、常见误区四个维度,全面解析 JSX 的使用方法,帮助你彻底掌握这一核心技能。

一、JSX 是什么?—— 不止是 “HTML+JS”

1. JSX 的本质:语法糖

JSX 是 Facebook 为 React 开发的一种语法扩展,其核心作用是简化 React 元素的创建。当我们编写 JSX 代码时,Babel(或 TypeScript)会将其编译为 React 的createElement函数调用(React 17 + 也支持更简洁的jsx/jsxs函数)。

举个例子

// 我们编写的JSX代码
const element = <h1 className="title">Hello, JSX!</h1>;

编译后的 JavaScript 代码(React 17 之前):

const element = React.createElement(
  'h1', // 元素类型
  { className: 'title' }, // 元素属性
  'Hello, JSX!' // 子元素
);

React 17 + 的编译结果(无需显式引入 React):

import { jsx as _jsx } from 'react/jsx-runtime';
const element = _jsx('h1', {
  className: 'title',
  children: 'Hello, JSX!'
});

从编译结果可以看出:JSX 最终会被转换为描述 UI 的 JavaScript 对象(React 元素) ,而不是直接渲染为 DOM 节点。这也是 JSX 能够与 JavaScript 逻辑无缝结合的根本原因。

2. 为什么要用 JSX?

在 JSX 出现之前,开发者需要通过React.createElement手动创建 UI 元素,代码冗长且可读性差。JSX 的出现解决了以下问题:

  • 直观性:类 HTML 的语法让 UI 结构一目了然,比纯 JavaScript 代码更易读、易维护;
  • 无缝集成逻辑:可以在 JSX 中直接嵌入 JavaScript 表达式,实现 UI 与业务逻辑的紧密结合;
  • 编译时检查:Babel 和 TypeScript 会在编译阶段检查 JSX 的语法错误,提前规避运行时问题;
  • 组件化支持:JSX 天然支持 React 组件的嵌套和组合,是 React 组件化思想的核心载体。

注意:JSX 并非 React 的强制要求,你可以始终使用React.createElement编写代码,但几乎所有 React 项目都会选择 JSX 以提升开发效率。

二、JSX 的核心语法规则:必掌握的基础

JSX 虽然看起来像 HTML,但本质是 JavaScript,因此有一套自己的语法规则。以下是最核心的规则,也是新手最容易踩坑的地方。

1. 标签必须闭合

与 HTML 不同,JSX 要求所有标签必须显式闭合,包括单标签(如<input><img>)。

// 错误:标签未闭合
const input = <input type="text">;
const img = <img src="logo.png">;

// 正确:单标签使用自闭合语法
const input = <input type="text" />;
const img = <img src="logo.png" alt="logo" />;

// 双标签必须成对出现
const div = <div>Hello, JSX</div>;

2. 只能有一个根元素

JSX 表达式中不能直接返回多个同级元素,必须用一个根元素包裹(或使用 Fragment 片段)。

// 错误:多个根元素
const App = () => {
  return (
    <h1>标题</h1>
    <p>内容</p>
  );
};

// 正确:用div作为根元素
const App = () => {
  return (
    <div>
      <h1>标题</h1>
      <p>内容</p>
    </div>
  );
};

3. 类名使用className而非class

在 JavaScript 中,class是关键字,因此 JSX 中不能使用class属性定义 CSS 类名,而是使用className(对应 DOM 的className属性)。

// 错误:使用class关键字
const element = <div class="container">Hello</div>;

// 正确:使用className
const element = <div className="container">Hello</div>;

补充:在 React Native 中,类名使用style属性,而不是className

4. 表单标签的for属性改为htmlFor

同理,for是 JavaScript 的关键字,JSX 中使用htmlFor替代<label>标签的for属性。

// 错误:使用for关键字
const label = <label for="username">用户名:</label>;

// 正确:使用htmlFor
const label = <label htmlFor="username">用户名:</label>;
<input id="username" type="text" />;

5. 内联样式是对象形式

JSX 中的内联样式不能直接写 CSS 字符串,而是需要传递一个样式对象,属性名采用驼峰命名法(如fontSize而非font-size)。

// 错误:CSS字符串形式
const element = <div style="font-size: 16px; color: red;">Hello</div>;

// 正确:样式对象形式
const element = <div style={{ fontSize: '16px', color: 'red' }}>Hello</div>;

// 推荐:将样式抽离为变量
const textStyle = {
  fontSize: '16px',
  color: 'red',
  marginTop: '10px' // 驼峰命名法
};
const element = <div style={textStyle}>Hello</div>;

6. 插入 JavaScript 表达式:使用{}

这是 JSX 最强大的特性之一:可以通过大括号{}在 JSX 中嵌入任意有效的 JavaScript 表达式(注意:是表达式,不是语句)。

// 1. 变量
const name = 'React';
const element = <h1>Hello, {name}!</h1>;

// 2. 算术运算
const a = 10;
const b = 20;
const element = <p>10 + 20 = {a + b}</p>;

// 3. 函数调用
const getGreeting = (name) => `Hello, ${name}!`;
const element = <h1>{getGreeting('JSX')}</h1>;

// 4. 三元运算符(条件表达式)
const isLogin = true;
const element = <p>{isLogin ? '已登录' : '请登录'}</p>;

// 5. 数组(会自动展开)
const list = ['苹果', '香蕉', '橙子'];
const element = <div>{list}</div>; // 渲染为:<div>苹果香蕉橙子</div>

注意:{}中只能放表达式(有返回值的代码),不能放语句(如 if、for、switch 等)。如果需要使用语句,需在 JSX 外部处理。

7. JSX 中的注释

JSX 中的注释需要写在{}内,格式为/* 注释内容 */(单行注释也可以用//,但需要注意换行)。

const element = (
  <div>
    {/* 这是JSX中的多行注释 */}
    <h1>Hello, JSX!</h1>
    {/* 单行注释也可以这样写 */}
    {/*
      多行注释
      可以换行
    */}
    <p>{/* 行内注释 */}这是内容</p>
  </div>
);

// 单行注释的另一种写法(注意换行)
const element = (
  <div>
    {/* 推荐 */}
    <h1>Hello, JSX!</h1>
    // 这种写法会报错,因为//不在{}内
    <p>{// 这种写法可行,但需要换行
      '内容'}</p>
  </div>
);

三、JSX 的进阶用法:从基础到实战

掌握了基础语法后,我们来看看 JSX 在实际开发中的高频进阶用法。

1. 片段(Fragment):避免多余的根节点

前面提到 JSX 必须有一个根元素,但有时我们不想添加额外的<div>等节点(避免 DOM 层级过深),此时可以使用React Fragment(片段),它会在渲染时被忽略,只保留子元素。

用法 1:<React.Fragment>

import React from 'react';

const App = () => {
  return (
    <React.Fragment>
      <h1>标题</h1>
      <p>内容</p>
      <button>按钮</button>
    </React.Fragment>
  );
};

用法 2:空标签<> </>(简写形式)

这是 React 16.2 + 支持的简写语法,功能与<React.Fragment>一致,但不支持添加属性(如 key)。

const App = () => {
  return (
    <>
      <h1>标题</h1>
      <p>内容</p>
      <button>按钮</button>
    </>
  );
};

用法 3:带 key 的 Fragment(仅支持完整写法)

当在列表中渲染 Fragment 时,需要为其添加 key 属性,此时必须使用完整的<React.Fragment>

const list = [
  { id: 1, text: '第一项' },
  { id: 2, text: '第二项' }
];

const App = () => {
  return (
    <div>
      {list.map(item => (
        <React.Fragment key={item.id}>
          <p>{item.text}</p>
          <hr />
        </React.Fragment>
      ))}
    </div>
  );
};

2. 列表渲染:使用map并添加key

在 JSX 中渲染列表(如数组)时,通常使用Array.prototype.map方法,且必须为每个列表项添加唯一的key属性

const todos = [
  { id: 1, text: '学习JSX' },
  { id: 2, text: '学习React' },
  { id: 3, text: '开发项目' }
];

const TodoList = () => {
  return (
    <ul>
      {todos.map(todo => (
        // 正确:使用唯一的id作为key
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
};

关于key的重要注意事项:

  • key的作用:帮助 React 识别列表中元素的变化(添加、删除、排序),从而优化渲染性能;
  • key必须是唯一的:在同一列表中,每个元素的 key 不能重复;
  • 不要使用索引作为 key:如果列表的顺序发生变化(如排序、删除),索引会重新分配,导致 React 误判元素变化,引发性能问题或渲染错误;
  • key只在列表内部有效:key 是给 React 看的,不会传递给组件,因此不能在组件内部通过props.key获取。

3. 条件渲染:多种实现方式

在 JSX 中实现条件渲染有多种方式,可根据场景选择:

方式 1:三元运算符(适合简单条件)

const isLogin = true;

const UserInfo = () => {
  return (
    <div>
      {isLogin ? (
        <p>欢迎回来,用户!</p>
      ) : (
        <button>请登录</button>
      )}
    </div>
  );
};

方式 2:逻辑与运算符&&(适合 “存在即渲染” 的场景)

const hasUnreadMsg = true;
const unreadCount = 5;

const MsgTip = () => {
  return (
    <div>
      {/* 当hasUnreadMsg为true时,渲染后面的元素;为false时,返回false,不渲染 */}
      {hasUnreadMsg && <span className="badge">{unreadCount}</span>}
    </div>
  );
};

方式 3:外部条件语句(适合复杂条件)

const UserRole = ({ role }) => {
  // 外部定义渲染逻辑
  let content;
  if (role === 'admin') {
    content = <p>管理员</p>;
  } else if (role === 'user') {
    content = <p>普通用户</p>;
  } else {
    content = <p>游客</p>;
  }

  return <div>{content}</div>;
};

方式 4:组件提取(适合极复杂的条件)

将不同条件的渲染逻辑提取为独立组件,让代码更清晰。

const AdminPanel = () => <p>管理员面板</p>;
const UserPanel = () => <p>用户面板</p>;
const GuestPanel = () => <p>游客面板</p>;

const Panel = ({ role }) => {
  switch (role) {
    case 'admin':
      return <AdminPanel />;
    case 'user':
      return <UserPanel />;
    default:
      return <GuestPanel />;
  }
};

4. 自定义组件的渲染:首字母大写

在 JSX 中渲染自定义 React 组件时,组件名必须以大写字母开头(这是 React 的约定,用于区分原生 HTML 标签)。

// 正确:组件名首字母大写
const Button = () => <button>自定义按钮</button>;

const App = () => {
  return (
    <div>
      <Button /> {/* 渲染自定义组件 */}
      <button>原生按钮</button> {/* 渲染原生HTML标签 */}
    </div>
  );
};

// 错误:组件名小写,React会将其视为原生HTML标签(不存在的标签会渲染为<div>或报错)
const button = () => <button>自定义按钮</button>;
const App = () => {
  return <button />; // 渲染原生<button>,而非自定义组件
};

5. 属性传递(Props):向组件传递数据

可以通过 JSX 的属性(props)向自定义组件传递数据,属性名同样采用驼峰命名法(如onClickdataId)。

// 子组件接收props
const Greeting = (props) => {
  return <h1>Hello, {props.name}!</h1>;
};

// 父组件传递props
const App = () => {
  return (
    <div>
      {/* 传递字符串属性 */}
      <Greeting name="React" />
      {/* 传递非字符串属性(需用{}包裹) */}
      <Greeting name={123} />
      {/* 传递布尔值 */}
      <Greeting isShow={true} />
      {/* 传递函数 */}
      <Greeting onButtonClick={() => alert('点击了')} />
      {/* 传递JSX元素(子元素,对应props.children) */}
      <Greeting>
        <p>这是子元素</p>
      </Greeting>
    </div>
  );
};

补充:props.children是一个特殊的 props,用于接收组件的子元素(如上面的<p>这是子元素</p>)。

6. 危险的 HTML 渲染:dangerouslySetInnerHTML

默认情况下,React 会转义 JSX 中的所有内容,防止 XSS 攻击(跨站脚本攻击)。但有时我们需要渲染原始的 HTML 字符串(如后端返回的富文本),此时可以使用dangerouslySetInnerHTML属性(注意:使用该属性存在安全风险,需确保内容是可信的)。

// 原始HTML字符串
const htmlContent = '<p style="color: red;">这是富文本内容</p>';

// 错误:React会转义HTML标签,渲染为纯文本
const element = <div>{htmlContent}</div>;

// 正确:使用dangerouslySetInnerHTML渲染原始HTML
const element = <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;

警告:不要将用户输入的内容直接通过 dangerouslySetInnerHTML 渲染,否则可能导致 XSS 攻击。如果必须渲染用户输入,需先进行 HTML 转义或过滤。

7. JSX 作为变量、返回值和参数

由于 JSX 最终会被编译为 JavaScript 对象,因此它可以作为变量存储、作为函数返回值、作为参数传递给函数。

// 1. 作为变量
const header = <h1>Hello, JSX</h1>;

// 2. 作为函数返回值
const getHeader = () => {
  return <h1>Hello, JSX</h1>;
};

// 3. 作为参数传递
const renderElement = (element) => {
  return <div>{element}</div>;
};

const App = () => {
  return renderElement(header);
};

四、JSX 的常见误区与避坑指南

即使是有经验的开发者,也可能在使用 JSX 时踩坑。以下是最常见的误区及解决方案:

误区 1:混淆 HTML 和 JSX 的语法差异

问题:使用classforstyle等 HTML 属性,导致语法错误或样式不生效。解决方案:牢记 JSX 的属性替换规则:

  • class → className
  • for → htmlFor
  • style → 驼峰命名的样式对象
  • 自定义属性:使用data-*前缀(如data-id),React 会保留这些属性。

误区 2:在{}中使用语句(而非表达式)

问题:在 JSX 的{}中写入 if、for、switch 等语句,导致编译错误。

jsx

// 错误:if是语句,不能放在{}内
const element = <div>{if (true) { return 'Hello' }}</div>;

解决方案:将语句移到 JSX 外部,或使用三元运算符、逻辑与等表达式替代。

误区 3:列表渲染忘记加key或使用索引作为key

问题:列表渲染时未添加key,控制台出现警告;或使用索引作为key,导致列表排序 / 删除时渲染异常。解决方案:使用唯一的 ID(如后端返回的 id、UUID)作为key;如果确实没有唯一 ID,可考虑生成唯一标识(如item.name + item.index),但尽量避免使用索引。

误区 4:过度使用dangerouslySetInnerHTML

问题:随意使用dangerouslySetInnerHTML渲染不可信内容,导致 XSS 攻击风险。解决方案

  • 尽量避免使用dangerouslySetInnerHTML
  • 如果必须使用,确保内容是可信的(如后端自己生成的富文本);
  • 对用户输入的内容进行 HTML 转义(如使用he库)。

误区 5:忽略 JSX 的大小写敏感

问题:将原生 HTML 标签大写(如<Div>),或自定义组件小写(如<button>),导致渲染错误。解决方案

  • 原生 HTML 标签:全小写(如<div><button>);
  • 自定义组件:首字母大写(如<Button><TodoList>)。

误区 6:直接修改propsstate后渲染 JSX

问题:修改propsstate的原始值(如数组的push、对象的属性赋值),导致 React 无法检测到变化,JSX 不更新。解决方案:遵循 React 的不可变原则,创建新的数组 / 对象(如使用concatmapspread运算符)。

五、JSX 的优势:为什么它能成为 React 的标配?

总结一下,JSX 之所以能成为 React 开发的核心工具,主要有以下优势:

  1. 直观性:类 HTML 的语法让 UI 结构与代码逻辑分离但又紧密结合,比纯 JavaScript 更易读;
  2. 灵活性:可以嵌入任意 JavaScript 表达式,实现复杂的逻辑渲染;
  3. 安全性:默认转义内容,防止 XSS 攻击;
  4. 组件化:天然支持 React 的组件化思想,便于复用和维护;
  5. 跨平台:不仅可以用于 Web 端的 DOM 渲染,还可以用于 React Native 的原生组件渲染(语法一致,底层渲染不同);
  6. 工具支持:Babel、TypeScript、ESLint 等工具对 JSX 有完善的支持,提升开发效率。

六、总结

JSX 是 React 开发的基础,它不是 HTML,也不是新的编程语言,而是 JavaScript 的语法糖。掌握 JSX 的核心语法规则(如标签闭合、className、表达式插入)、进阶用法(如 Fragment、列表渲染、条件渲染)和避坑指南,是编写高效、可维护的 React 代码的关键。

值得一提的是,JSX 并非 React 的专属特性,Vue 3 也支持 JSX 语法,甚至一些其他前端框架也开始兼容 JSX。因此,学好 JSX 不仅能提升 React 开发能力,也是前端工程师的通用技能。

最后,记住:JSX 的本质是 JavaScript,所有 JavaScript 的特性都可以与 JSX 结合使用。不要被类 HTML 的语法迷惑,始终以 JavaScript 的思维来编写 JSX。

❌
❌