【从Vue3到React】Day 1: React基础概念

Day 1: React基础概念

学习时间:6-8小时 适合对象:有Vue3经验的开发者

📋 今日目标

  • ✅ 掌握React项目创建和结构
  • ✅ 熟练使用JSX语法
  • ✅ 理解函数组件和Props
  • ✅ 实现条件渲染和列表渲染
  • ✅ 完成第一个React项目

🌅 上午课程(3-4小时)

1️⃣ React简介和环境搭建(1小时)

React是什么?

React是一个用于构建用户界面的JavaScript库,由Facebook开发。核心特点:

  • 声明式:描述UI应该是什么样,React负责如何实现
  • 组件化:UI由独立、可复用的组件构成
  • 一次学习,随处编写:可用于Web、移动端、桌面端
React vs Vue 设计思想对比
特性 Vue React
模板语法 HTML模板 + 指令 JSX (JavaScript + XML)
响应式 自动依赖追踪 手动管理状态
学习曲线 平缓 稍陡(需要深入JS)
灵活性 中等 很高(一切皆JS)
官方路由/状态管理 有(Vue Router/Pinia) 社区方案为主
创建你的第一个React项目

使用Vite创建项目(比Create React App更快):

bash 复制代码
# 创建项目
npm create vite@latest my-react-app -- --template react

# 进入项目目录
cd my-react-app

# 安装依赖
npm install

# 启动开发服务器
npm run dev

项目创建过程

bash 复制代码
npm create vite@latest my-react-app -- --template react
Need to install the following packages:
create-vite@8.0.1
Ok to proceed? (y) y


> npx
> create-vite my-react-app --template react

|
o  Use rolldown-vite (Experimental)?:
|  No
|
o  Install with npm and start now?
|  Yes
|
o  Scaffolding project in C:\MyLearn\my-react-app...
|
o  Installing dependencies with npm...

added 153 packages, and audited 154 packages in 17s

32 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
|
o  Starting dev server...

> my-react-app@0.0.0 dev
> vite


  VITE v7.1.7  ready in 374 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

访问 http://localhost:5173/

项目结构解读
csharp 复制代码
my-react-app/
├── node_modules/       # 依赖包
├── public/             # 静态资源
├── src/                # 源代码目录
│   ├── assets/         # 资源文件(图片、样式等)
│   ├── App.jsx         # 根组件
│   ├── App.css         # 根组件样式
│   ├── main.jsx        # 入口文件(类似Vue的main.js)
│   └── index.css       # 全局样式
├── index.html          # HTML模板
├── package.json        # 项目配置
└── vite.config.js      # Vite配置

实际初始化后,项目结构截图

关键文件解析

main.jsx(入口文件):

jsx 复制代码
/**
 * 从 react 包中导入 StrictMode 组件
 * StrictMode 是 React 的严格模式组件,类似于 Vue 3 的开发模式检查
 * 它会在【开发环境】下进行额外的检查和警告,帮助你发现潜在问题
 * 不会渲染任何可见的 UI,也不会影响生产构建
 */
import { StrictMode } from 'react'

/**
 * 从 react-dom/client 包中导入 createRoot 方法
 * 这是 React 18 新的渲染 API,类似于 Vue 3 的 createApp()
 * 用于创建一个 React 根节点,然后将组件渲染到 DOM 中
 *
 * 对比 Vue 3:
 * Vue 3: createApp(App).mount('#app')
 * React: createRoot(document.getElementById('root')).render(<App />)
 */
import { createRoot } from 'react-dom/client'

/**
 * 导入全局样式文件
 * 类似于 Vue 3 中在 main.js 里 import './style.css'
 */
import './index.css'

/**
 * 导入根组件 App
 * 类似于 Vue 3 中的 import App from './App.vue'
 * 注意:React 组件文件通常使用 .jsx 或 .js 扩展名
 */
import App from './App.jsx'

/**
 * React 应用的启动流程(链式调用):
 *
 * 1. createRoot() - 创建 React 根节点
 *    参数:DOM 元素,这里获取 id 为 'root' 的 div
 *
 * 2. .render() - 将组件渲染到根节点
 *    参数:要渲染的 React 元素(JSX)
 *
 * 对比 Vue 3 的启动方式:
 *
 * Vue 3:
 * createApp(App).mount('#app')
 *
 * React 18:
 * createRoot(document.getElementById('root')).render(<App />)
 *
 * 主要区别:
 * - Vue 使用字符串选择器 '#app'
 * - React 需要传入实际的 DOM 元素对象
 */
createRoot(document.getElementById('root')).render(
  /**
   * StrictMode 包裹根组件
   * 在开发模式下会:
   * 1. 识别不安全的生命周期方法
   * 2. 检测意外的副作用
   * 3. 检测过时的 API
   * 4. 组件会渲染两次(仅开发模式),帮助发现副作用问题
   *
   * 类似于 Vue 3 开发工具的警告功能,但更严格
   */
  <StrictMode>
    {/*
      渲染 App 根组件
      JSX 语法:类似于 Vue 的模板语法,但实际上是 JavaScript
      <App /> 等同于 React.createElement(App)

      对比:
      Vue 3 模板: <App />
      React JSX: <App />

      看起来相同,但 React 的 JSX 需要编译成 JavaScript 函数调用
    */}
    <App />
  </StrictMode>,
)

App.jsx(根组件):

jsx 复制代码
/**
 * 从 react 包中导入 useState Hook
 *
 * Hook 是 React 16.8 引入的新特性,让你在函数组件中使用状态和其他 React 特性
 *
 * 对比 Vue 3:
 * Vue 3 Composition API: import { ref } from 'vue'
 * React Hooks: import { useState } from 'react'
 *
 * 相似点:都是在函数式组件中管理状态
 * 不同点:
 * - Vue 3 使用 ref() 创建响应式数据
 * - React 使用 useState() 创建状态
 */
import { useState } from 'react'

/**
 * 导入图片资源
 *
 * 在 Vite 中,可以直接导入图片文件
 * 导入后得到的是图片的 URL 路径字符串
 *
 * 类似于 Vue 3 中:
 * import logo from './assets/logo.png'
 */
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg' // 以 / 开头表示从 public 目录导入

/**
 * 导入组件样式
 * 这个样式只作用于当前组件相关的元素
 */
import './App.css'

/**
 * App 组件 - 函数组件写法
 *
 * React 组件的两种写法:
 * 1. 函数组件(推荐,现代写法)
 * 2. 类组件(旧写法,逐渐被淘汰)
 *
 * 对比 Vue 3:
 *
 * Vue 3 组件:
 * <script setup>
 * import { ref } from 'vue'
 * const count = ref(0)
 * </script>
 *
 * React 函数组件:
 * function App() {
 *   const [count, setCount] = useState(0)
 *   return (...)
 * }
 *
 * 主要区别:
 * - Vue 3 使用 <script setup> 语法糖,代码更简洁
 * - React 需要显式返回 JSX
 * - React 组件名必须大写开头(Pascal命名)
 */
function App() {
  /**
   * useState Hook - 状态管理
   *
   * 语法:const [状态变量, 更新函数] = useState(初始值)
   *
   * useState 返回一个数组,包含两个元素:
   * 1. count - 当前状态值(类似 Vue 的 count.value)
   * 2. setCount - 更新状态的函数(类似 Vue 的 count.value = newValue)
   *
   * 对比 Vue 3:
   *
   * Vue 3:
   * const count = ref(0)           // 创建响应式数据
   * count.value++                  // 修改需要 .value
   *
   * React:
   * const [count, setCount] = useState(0)  // 创建状态
   * setCount(count + 1)            // 通过 setCount 函数修改
   *
   * 重要区别:
   * - Vue 3 的 ref 是响应式对象,直接修改 .value
   * - React 的 state 是不可变的,必须通过 setState 函数更新
   * - React 的状态更新是异步的,可能会合并多次更新
   */
  const [count, setCount] = useState(0)

  /**
   * 返回 JSX (组件的渲染内容)
   *
   * JSX 是 JavaScript XML 的缩写,是 React 的模板语法
   * 看起来像 HTML,但实际是 JavaScript 表达式
   *
   * 对比 Vue 3:
   * Vue 3 使用 <template> 标签包裹模板
   * React 直接在 return 中返回 JSX
   */
  return (
    /**
     * Fragment (片段) - 空标签 <>...</>
     *
     * 用途:包裹多个子元素,但不会在 DOM 中创建额外节点
     *
     * 对比 Vue 3:
     * Vue 3: 可以直接写多个根元素(Vue 3.2+)
     * React: 必须有一个根元素,可以用 Fragment 避免额外的 div
     *
     * 完整写法:<React.Fragment>...</React.Fragment>
     * 简写:<>...</>
     *
     * 类似于 Vue 3 的 <template> 标签(但不完全一样)
     */
    <>
      <div>

        {/*
          target="_blank" 需要配合 rel="noopener noreferrer" 使用
          但这里省略了,不是最佳实践
        */}
        <a href="https://vite.dev" target="_blank">
          {/*
            JSX 中使用动态值需要用 {} 包裹

            属性对比:
            - class 在 JSX 中要写成 className (因为 class 是 JS 关键字)
            - for 在 JSX 中要写成 htmlFor

            Vue 3 vs React:
            Vue 3: <img :src="viteLogo" class="logo" />
            React: <img src={viteLogo} className="logo" />

            主要区别:
            - Vue 用 : 绑定动态属性
            - React 用 {} 包裹 JavaScript 表达式
          */}
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>

      <h1>Vite + React</h1>

      <div className="card">
        {/*
          事件处理 - onClick

          语法:onClick={事件处理函数}

          这里使用箭头函数调用 setCount:
          onClick={() => setCount((count) => count + 1)}

          详细解析:
          1. onClick={...} - 绑定点击事件
          2. () => ... - 箭头函数,点击时执行
          3. setCount(...) - 调用状态更新函数
          4. (count) => count + 1 - 函数式更新,推荐写法

          为什么用函数式更新?
          setCount(count + 1)     // ❌ 可能出现问题(基于旧值)
          setCount(c => c + 1)    // ✅ 推荐(总是基于最新值)

          对比 Vue 3:

          Vue 3:
          <button @click="count++">{{ count }}</button>
          或
          <button @click="increment">{{ count }}</button>

          React:
          <button onClick={() => setCount(c => c + 1)}>
            count is {count}
          </button>

          主要区别:
          - Vue 3 使用 @click 或 v-on:click
          - React 使用 onClick (驼峰命名)
          - Vue 3 可以直接修改响应式数据
          - React 必须使用 setState 函数
          - Vue 3 用 {{}} 插值
          - React 用 {} 插值
        */}
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>

        <p>
          Edit <code>src/App.jsx</code> and save to test HMR
        </p>
      </div>

      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  )
}

/**
 * 默认导出组件
 *
 * React 组件的导出方式:
 * 1. 默认导出:export default App (推荐)
 * 2. 命名导出:export { App } 或 export function App() {}
 *
 * 对比 Vue 3:
 * Vue 3 使用 <script setup> 时不需要显式导出
 * React 必须显式导出组件
 */
export default App
安装React DevTools

在Chrome或Edge浏览器中安装React DevTools扩展,用于调试React应用。

实践任务:

  1. 创建一个新的React项目
  2. 启动开发服务器
  3. 修改App.jsx中的内容,观察热更新效果
  4. 打开React DevTools查看组件树

效果演示:


2️⃣ JSX语法深入(1.5小时)

什么是JSX?

JSX = JavaScript + XML,是JavaScript的语法扩展 ,允许在JS中写类似HTML的标记。

关键理解:

  • JSX不是字符串,也不是HTML
  • JSX会被编译成React.createElement()调用
  • JSX本质上就是JavaScript表达式
JSX基础语法

1. 嵌入JavaScript表达式

jsx 复制代码
const name = '訾博'
const age = 25

function Welcome() {
  return (
    <div>
      <h1>你好,{name}!</h1>
      <p>你今年 {age} 岁</p>
      <p>明年你将 {age + 1} 岁</p>
      <p>当前时间:{new Date().toLocaleTimeString()}</p>
    </div>
  )
}

2. JSX中的属性

jsx 复制代码
// 字符串属性:直接用引号
<img src="avatar.jpg" alt="头像" />

// 动态属性:用花括号
const imageUrl = 'avatar.jpg'
<img src={imageUrl} alt="头像" />

// 注意:class要写成className(因为class是JS关键字)
<div className="container">内容</div>

// style是对象而不是字符串
<div style={{ color: 'red', fontSize: 16, marginTop: '10px' }}>
  红色文字
</div>

3. 条件渲染(没有v-if)

jsx 复制代码
// 方式1:三元表达式
function Greeting({ isLogin }) {
  return (
    <div>
      {isLogin ? <h1>欢迎回来!</h1> : <h1>请先登录</h1>}
    </div>
  )
}

// 方式2:逻辑与运算符(适合只显示或不显示)
function Notification({ count }) {
  return (
    <div>
      {count > 0 && <span>你有 {count} 条新消息</span>}
    </div>
  )
}

// 方式3:提前return(适合多条件)
function UserStatus({ status }) {
  if (status === 'loading') {
    return <div>加载中...</div>
  }

  if (status === 'error') {
    return <div>出错了!</div>
  }

  return <div>加载完成</div>
}

// 方式4:立即执行函数(复杂逻辑)
function ComplexComponent({ value }) {
  return (
    <div>
      {(() => {
        if (value > 100) return '很大'
        if (value > 50) return '中等'
        return '很小'
      })()}
    </div>
  )
}

4. 列表渲染(没有v-for)

jsx 复制代码
function TodoList() {
  const todos = [
    { id: 1, text: '学习React', done: false },
    { id: 2, text: '做项目', done: false },
    { id: 3, text: '休息', done: true }
  ]

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          {todo.text} {todo.done ? '✅' : '⏳'}
        </li>
      ))}
    </ul>
  )
}

⚠️ key的重要性:

  • key帮助React识别哪些元素改变了
  • key必须在兄弟节点中唯一
  • 不要用index作为key(除非列表不会重排)
  • key应该是稳定的、可预测的
JSX与Vue Template对比
Vue Template React JSX 说明
{{ message }} {message} 插值表达式
v-if="isShow" {isShow && <div />} 条件渲染
v-for="item in list" {list.map(item => ...)} 列表渲染
:class="className" className={className} 动态class
:style="styleObj" style={styleObj} 动态style
@click="handler" onClick={handler} 事件绑定
v-model="value" value={value} onChange={handler} 双向绑定

实践任务:

  1. 创建一个显示个人信息的组件,包含姓名、年龄、技能列表
  2. 根据年龄显示不同的称呼(未成年/成年)
  3. 用不同颜色标记不同技能等级

实践代码:

jsx 复制代码
import { useState } from "react";

function UserInfo() {

    const [user] = useState({
        name: '訾博',
        age: 29,
        skills: [{ id: 1, name: '阅读' }, { id: 2, name: '编程' }, { id: 3, name: '画图' }]
    })

    return (
        <>
            <p>姓名:{user.name}</p>
            <p>年龄:{user.age}, 是否成年: { user.age > 18 ? '成年' : '未成年' }</p>
            <p>技能列表:</p>
            <ul>
                {
                    user.skills.map(skill => (
                        <li key={skill.id}>{skill.name}</li>
                    ))
                }
            </ul>
        </>
    );
}

export default UserInfo;

实践结果:


3️⃣ 组件基础(1.5小时)

函数组件

现代React推荐使用函数组件(配合Hooks),不再使用类组件。

基础组件定义:

jsx 复制代码
// 组件名必须大写开头
function Welcome() {
  return <h1>Hello, React!</h1>
}

// 导出组件
export default Welcome

// 或者使用箭头函数
const Welcome = () => {
  return <h1>Hello, React!</h1>
}

export default Welcome
Props传递

Props类似于Vue的props,用于父组件向子组件传递数据。

基础用法:

jsx 复制代码
// 父组件
function App() {
  return (
    <div>
      <Greeting name="訾博" age={25} />
      <Greeting name="张三" age={30} />
    </div>
  )
}

// 子组件(接收props)
function Greeting(props) {
  return (
    <div>
      <h2>你好,{props.name}!</h2>
      <p>年龄:{props.age}</p>
    </div>
  )
}

Props解构(推荐):

jsx 复制代码
// 直接解构props
function Greeting({ name, age }) {
  return (
    <div>
      <h2>你好,{name}!</h2>
      <p>年龄:{age}</p>
    </div>
  )
}

// 设置默认值
function Greeting({ name = '游客', age = 0 }) {
  return (
    <div>
      <h2>你好,{name}!</h2>
      <p>年龄:{age}</p>
    </div>
  )
}

// 使用剩余参数
function Button({ children, ...restProps }) {
  return <button {...restProps}>{children}</button>
}

// 使用方式:<Button className="primary" onClick={handler}>点击</Button>

Props的特点:

  1. 只读性:不能修改props(单向数据流)

    jsx 复制代码
    function Greeting({ name }) {
      // ❌ 错误:不能修改props
      // name = '新名字'
    
      // ✅ 正确:需要修改时用state(明天学习)
      return <h1>{name}</h1>
    }
  2. 可以传递任何类型

    jsx 复制代码
    <Component
      string="文本"
      number={123}
      boolean={true}
      array={[1, 2, 3]}
      object={{ name: '张三' }}
      function={() => console.log('click')}
      element={<span>元素</span>}
    />
  3. children特殊prop

    jsx 复制代码
    function Card({ children }) {
      return (
        <div className="card">
          {children}
        </div>
      )
    }
    
    // 使用
    <Card>
      <h2>标题</h2>
      <p>内容</p>
    </Card>
组件组合

React推崇组合而非继承。

jsx 复制代码
// 容器组件
function Container({ children }) {
  return <div className="container">{children}</div>
}

// 布局组件
function Layout({ header, sidebar, content }) {
  return (
    <div className="layout">
      <header>{header}</header>
      <aside>{sidebar}</aside>
      <main>{content}</main>
    </div>
  )
}

// 使用
function App() {
  return (
    <Layout
      header={<Header />}
      sidebar={<Sidebar />}
      content={<Content />}
    />
  )
}

实践任务:

  1. 创建一个UserCard组件,接收用户信息作为props
  2. 创建一个Button组件,可以接收不同的样式类型
  3. 创建一个Card容器组件,可以包裹任意内容

实践代码:

jsx 复制代码
// UserCard组件
function UserCard({ name, age, skills}) {
    return (
        <>
            <p>姓名:{name}</p>
            <p>年龄:{age}, 是否成年: { age > 18 ? '成年' : '未成年' }</p>
            <p>技能列表:</p>
            <ul>
                {
                    skills.map(skill => (
                        <li key={skill.id}>{skill.}</li>
                    ))
                }
            </ul>
        </>
    );
}

export default UserCard

// Button组件
function Button({ func, ele}) {
    return (
        <>
            <button onClick={func}>{ele}</button>
        </>
    );
}

export default Button

// Card组件
function Card({ title, children }) {
    return (
        <>
            <div className="card-title">{title}</div>
            <div className="card-body">{children}</div>
        </>
    );
}

export default Card

// 使用演示代码
import './App.css'

import UserCard from './components/UserCard'
import Button from './components/Button'
import Card from './components/Card'

function App() {
  return (
    <>
      {/* UserCard */}
      <UserCard name="訾博" age="29" skills={
        [
          { id: 1, name: '阅读' },
          { id: 2, name: '编程' },
          { id: 3, name: '画图' }
        ]
      } />
      {/* Button */}
      <Button func={() => alert('按钮被点击了')} ele={<span>点我</span>} />
      {/* Card */}
      <Card title="卡片标题">
        <p>这是卡片的内容,可以是任意元素。</p>
        <p>可以通过 children 属性传递内容。</p>
      </Card>
    </>
  )
}

export default App

实践结果:


🌆 下午课程(3-4小时)

4️⃣ 条件渲染实战(1小时)

多条件渲染
jsx 复制代码
function OrderStatus({ status }) {
  // 方式1:多个if语句
  if (status === 'pending') {
    return <span className="status-pending">待支付</span>
  }
  if (status === 'paid') {
    return <span className="status-paid">已支付</span>
  }
  if (status === 'shipped') {
    return <span className="status-shipped">已发货</span>
  }
  if (status === 'completed') {
    return <span className="status-completed">已完成</span>
  }
  return <span className="status-unknown">未知状态</span>
}

// 方式2:switch语句
function OrderStatus({ status }) {
  let content

  switch (status) {
    case 'pending':
      content = <span className="status-pending">待支付</span>
      break
    case 'paid':
      content = <span className="status-paid">已支付</span>
      break
    case 'shipped':
      content = <span className="status-shipped">已发货</span>
      break
    case 'completed':
      content = <span className="status-completed">已完成</span>
      break
    default:
      content = <span className="status-unknown">未知状态</span>
  }

  return content
}

// 方式3:对象映射(推荐)
function OrderStatus({ status }) {
  const statusMap = {
    pending: <span className="status-pending">待支付</span>,
    paid: <span className="status-paid">已支付</span>,
    shipped: <span className="status-shipped">已发货</span>,
    completed: <span className="status-completed">已完成</span>
  }

  return statusMap[status] || <span className="status-unknown">未知状态</span>
}
条件渲染最佳实践
jsx 复制代码
// ✅ 好的做法:清晰的条件逻辑
function ProductCard({ product, isLoggedIn, isPremium }) {
  // 提前处理复杂条件
  const canPurchase = isLoggedIn && product.stock > 0
  const showDiscount = isPremium && product.discount > 0

  return (
    <div className="product-card">
      <h3>{product.name}</h3>
      <p className="price">¥{product.price}</p>

      {showDiscount && (
        <span className="discount">-{product.discount}%</span>
      )}

      {canPurchase ? (
        <button>立即购买</button>
      ) : (
        <button disabled>暂不可购买</button>
      )}
    </div>
  )
}

// ❌ 避免:过于复杂的嵌套条件
function BadExample({ a, b, c, d }) {
  return (
    <div>
      {a ? (
        b ? (
          c ? <ComponentA /> : <ComponentB />
        ) : (
          d ? <ComponentC /> : <ComponentD />
        )
      ) : (
        <ComponentE />
      )}
    </div>
  )
}

实践任务: 创建一个根据时间显示不同问候语的组件,早上/中午/晚上显示不同内容和样式

实践代码:

jsx 复制代码
// TimeShow 组件
function TimeShow({ hour }) {
    const morning = <p style={{
        color: 'orange'
    }}>上午好</p>
    const afternoon = <p style={{
        color: 'blue'
    }}>下午好</p>
    const night = <p style={{
        color: 'black'
    }}>晚上好</p>

    return (
        <>
            <div>当前时间:{hour}点</div>
            <div>
                {hour < 12 ? morning : hour < 18 ? afternoon : night}
            </div>
        </>
    )
}

export default TimeShow

// return 语句写法2
return (
    <>
        <div>当前时间:{hour}点</div>
        <div>
            {
            (() => {
                if (hour < 12) {
                    return morning
                } else if (hour < 18) {
                    return afternoon
                } else {
                    return night
                }
            })()
        }
        </div>
    </>
)

// 组件使用
import './App.css'

import TimeShow from './components/TimeShow'

function App() {
  return (
    <>
      {/* TimeShow */}
      <TimeShow hour='9' />
      <TimeShow hour='15' />
      <TimeShow hour='21' />
    </>
  )
}

export default App

实践结果:


5️⃣ 列表渲染实战(1小时)

基础列表渲染
jsx 复制代码
function UserList() {
  const users = [
    { id: 1, name: '张三', age: 25, role: 'admin' },
    { id: 2, name: '李四', age: 30, role: 'user' },
    { id: 3, name: '王五', age: 28, role: 'user' }
  ]

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          {user.name} - {user.age}岁 - {user.role}
        </li>
      ))}
    </ul>
  )
}
列表过滤
jsx 复制代码
function FilteredUserList() {
  const users = [
    { id: 1, name: '张三', age: 25, active: true },
    { id: 2, name: '李四', age: 30, active: false },
    { id: 3, name: '王五', age: 28, active: true }
  ]

  // 过滤出活跃用户
  const activeUsers = users.filter(user => user.active)

  return (
    <ul>
      {activeUsers.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}
列表排序
jsx 复制代码
function SortedProductList() {
  const products = [
    { id: 1, name: '商品A', price: 99 },
    { id: 2, name: '商品B', price: 199 },
    { id: 3, name: '商品C', price: 49 }
  ]

  // 按价格排序
  const sortedProducts = [...products].sort((a, b) => a.price - b.price)

  return (
    <ul>
      {sortedProducts.map(product => (
        <li key={product.id}>
          {product.name} - ¥{product.price}
        </li>
      ))}
    </ul>
  )
}
空列表处理
jsx 复制代码
function ProductList({ products }) {
  // 处理空列表
  if (products.length === 0) {
    return <div className="empty">暂无商品</div>
  }

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  )
}

// 或者使用条件渲染
function ProductList({ products }) {
  return (
    <>
      {products.length === 0 ? (
        <div className="empty">暂无商品</div>
      ) : (
        <ul>
          {products.map(product => (
            <li key={product.id}>{product.name}</li>
          ))}
        </ul>
      )}
    </>
  )
}
复杂列表示例
jsx 复制代码
function StudentList() {
  const students = [
    { id: 1, name: '张三', score: 85, passed: true },
    { id: 2, name: '李四', score: 92, passed: true },
    { id: 3, name: '王五', score: 58, passed: false },
    { id: 4, name: '赵六', score: 75, passed: true }
  ]

  // 统计信息
  const passedCount = students.filter(s => s.passed).length
  const avgScore = students.reduce((sum, s) => sum + s.score, 0) / students.length

  return (
    <div>
      <div className="summary">
        <p>总人数:{students.length}</p>
        <p>及格人数:{passedCount}</p>
        <p>平均分:{avgScore.toFixed(2)}</p>
      </div>

      <table>
        <thead>
          <tr>
            <th>姓名</th>
            <th>分数</th>
            <th>状态</th>
          </tr>
        </thead>
        <tbody>
          {students.map(student => (
            <tr key={student.id} className={student.passed ? 'passed' : 'failed'}>
              <td>{student.name}</td>
              <td>{student.score}</td>
              <td>{student.passed ? '及格' : '不及格'}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

实践任务: 创建一个商品列表组件,支持按价格过滤、排序,显示库存状态

实践代码:

jsx 复制代码
// 商品列表组件
function GoodList({ tag }) {
    // 商品列表数据
    const goods = [
        { id: 1, name: '苹果', price: 150, inStock: true },
        { id: 2, name: '香蕉', price: 100, inStock: false },
        { id: 3, name: '橘子', price: 50, inStock: true }
    ]

    // 按照价格从低到高排序
    const sg = [...goods].sort((a, b) => a.price - b.price);

    // 过滤出有库存的商品
    const filteredGoods = [...goods].filter(good => good.inStock);

    // 筛选出价格在100元以下的商品
    const cheapGoods = [...goods].filter(good => good.price < 100);

    return (
        <div>
            {
                tag === 1 ? (
                    <>
                        <p>按照价格从低到高排序</p>
                        <ul>
                            {sg.map(good => (
                                <li key={good.id}>{good.name} - ¥{good.price} - {good.inStock ? '有货' : '无货'}</li>
                            ))}
                        </ul>
                    </>
                ) : tag === 2 ? (
                    <>
                        <p>过滤出有库存的商品</p>
                        <ul>
                            {filteredGoods.map(good => (
                                <li key={good.id}>{good.name} - ¥{good.price} - {good.inStock ? '有货' : '无货'}</li>
                            ))}
                        </ul>
                    </>
                ) : (
                    <>
                        <p>筛选出价格在100元以下的商品</p>
                        <ul>
                            {cheapGoods.map(good => (
                                <li key={good.id}>{good.name} - ¥{good.price} - {good.inStock ? '有货' : '无货'}</li>
                            ))}
                        </ul>
                    </>
                )
            }
        </div>
    )
}

export default GoodList

// 使用演示
import './App.css'

import GoodList from './components/GoodList'

function App() {
  return (
    <>
      {/* GoodList */}
      <GoodList tag={1} />
      <GoodList tag={2} />
      <GoodList tag={3} />
    </>
  )
}

export default App

实践结果:


6️⃣ 实践项目:待办事项列表(2小时)

项目需求

创建一个静态版本的待办事项列表应用,包含以下功能:

  1. 显示待办事项列表
  2. 显示完成/未完成状态
  3. 统计总数、已完成数、未完成数
  4. 根据状态筛选显示(全部/未完成/已完成)
  5. 优先级标签显示
组件结构设计
scss 复制代码
TodoApp (容器组件)
├── Header (头部标题)
├── TodoInput (输入框 - 暂不实现功能)
├── FilterBar (筛选栏)
├── TodoList (列表容器)
│   └── TodoItem (单个待办项)
└── Footer (统计信息)
完整代码实现

准备数据(App.jsx):

jsx 复制代码
import './App.css'
import Header from './components/Header'
import TodoInput from './components/TodoInput'
import FilterBar from './components/FilterBar'
import TodoList from './components/TodoList'
import Footer from './components/Footer'

function App() {
  // 模拟数据(明天会用useState管理)
  const todos = [
    { id: 1, text: '学习React基础', completed: true, priority: 'high' },
    { id: 2, text: '完成待办事项项目', completed: false, priority: 'high' },
    { id: 3, text: '阅读React文档', completed: false, priority: 'medium' },
    { id: 4, text: '练习JSX语法', completed: true, priority: 'low' },
    { id: 5, text: '理解组件和Props', completed: false, priority: 'medium' }
  ]

  // 当前筛选条件(明天会用useState管理)
  const currentFilter = 'all' // all | active | completed

  // 根据筛选条件过滤todos
  const filteredTodos = todos.filter(todo => {
    if (currentFilter === 'active') return !todo.completed
    if (currentFilter === 'completed') return todo.completed
    return true
  })

  // 统计数据
  const stats = {
    total: todos.length,
    completed: todos.filter(t => t.completed).length,
    active: todos.filter(t => !t.completed).length
  }

  return (
    <div className="app">
      <Header />
      <div className="todo-container">
        <TodoInput />
        <FilterBar currentFilter={currentFilter} />
        <TodoList todos={filteredTodos} />
        <Footer stats={stats} />
      </div>
    </div>
  )
}

export default App

创建components目录和各个组件:

components/Header.jsx:

jsx 复制代码
function Header() {
  return (
    <header className="header">
      <h1>📝 我的待办事项</h1>
      <p>今天也要加油哦!</p>
    </header>
  )
}

export default Header

components/TodoInput.jsx:

jsx 复制代码
function TodoInput() {
  return (
    <div className="todo-input">
      <input
        type="text"
        placeholder="添加新的待办事项..."
        disabled
      />
      <button disabled>添加</button>
      <p className="hint">💡 明天我们会让它动起来!</p>
    </div>
  )
}

export default TodoInput

components/FilterBar.jsx:

jsx 复制代码
function FilterBar({ currentFilter }) {
  const filters = [
    { key: 'all', label: '全部' },
    { key: 'active', label: '未完成' },
    { key: 'completed', label: '已完成' }
  ]

  return (
    <div className="filter-bar">
      {filters.map(filter => (
        <button
          key={filter.key}
          className={currentFilter === filter.key ? 'active' : ''}
          disabled
        >
          {filter.label}
        </button>
      ))}
    </div>
  )
}

export default FilterBar

components/TodoList.jsx:

jsx 复制代码
import TodoItem from './TodoItem'

function TodoList({ todos }) {
  if (todos.length === 0) {
    return (
      <div className="empty-state">
        <p>🎉 暂无待办事项</p>
      </div>
    )
  }

  return (
    <ul className="todo-list">
      {todos.map(todo => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </ul>
  )
}

export default TodoList

components/TodoItem.jsx:

jsx 复制代码
function TodoItem({ todo }) {
  // 优先级颜色映射
  const priorityColors = {
    high: '#ff4d4f',
    medium: '#faad14',
    low: '#52c41a'
  }

  // 优先级文本映射
  const priorityLabels = {
    high: '高',
    medium: '中',
    low: '低'
  }

  return (
    <li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
      <div className="todo-check">
        <input
          type="checkbox"
          checked={todo.completed}
          disabled
        />
      </div>

      <div className="todo-content">
        <span className="todo-text">{todo.text}</span>
        <span
          className="todo-priority"
          style={{
            backgroundColor: priorityColors[todo.priority],
            color: 'white',
            padding: '2px 8px',
            borderRadius: '4px',
            fontSize: '12px'
          }}
        >
          {priorityLabels[todo.priority]}优先级
        </span>
      </div>

      <div className="todo-actions">
        <button className="btn-delete" disabled>删除</button>
      </div>
    </li>
  )
}

export default TodoItem

components/Footer.jsx:

jsx 复制代码
function Footer({ stats }) {
  return (
    <footer className="footer">
      <div className="stats">
        <span>总计:<strong>{stats.total}</strong></span>
        <span>已完成:<strong>{stats.completed}</strong></span>
        <span>未完成:<strong>{stats.active}</strong></span>
      </div>

      <div className="progress">
        <div className="progress-bar">
          <div
            className="progress-fill"
            style={{
              width: `${stats.total > 0 ? (stats.completed / stats.total * 100) : 0}%`
            }}
          />
        </div>
        <span className="progress-text">
          完成度:{stats.total > 0 ? Math.round(stats.completed / stats.total * 100) : 0}%
        </span>
      </div>
    </footer>
  )
}

export default Footer
样式文件(App.css)
css 复制代码
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  padding: 20px;
}

.app {
  max-width: 800px;
  margin: 0 auto;
}

/* Header */
.header {
  text-align: center;
  color: white;
  margin-bottom: 30px;
}

.header h1 {
  font-size: 48px;
  margin-bottom: 10px;
}

.header p {
  font-size: 18px;
  opacity: 0.9;
}

/* Todo Container */
.todo-container {
  background: white;
  border-radius: 16px;
  padding: 30px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}

/* Todo Input */
.todo-input {
  margin-bottom: 20px;
}

.todo-input input {
  width: calc(100% - 90px);
  padding: 12px 16px;
  font-size: 16px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  outline: none;
  transition: border-color 0.3s;
}

.todo-input input:focus {
  border-color: #667eea;
}

.todo-input input:disabled {
  background: #f5f5f5;
  cursor: not-allowed;
}

.todo-input button {
  width: 80px;
  padding: 12px;
  margin-left: 10px;
  font-size: 16px;
  background: #667eea;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.3s;
}

.todo-input button:hover:not(:disabled) {
  background: #5568d3;
}

.todo-input button:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.todo-input .hint {
  margin-top: 10px;
  font-size: 14px;
  color: #999;
}

/* Filter Bar */
.filter-bar {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
  padding-bottom: 20px;
  border-bottom: 2px solid #f0f0f0;
}

.filter-bar button {
  flex: 1;
  padding: 10px;
  font-size: 14px;
  background: #f5f5f5;
  border: 2px solid transparent;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.3s;
}

.filter-bar button.active {
  background: #667eea;
  color: white;
  border-color: #667eea;
}

.filter-bar button:hover:not(:disabled):not(.active) {
  background: #e8e8e8;
}

/* Todo List */
.todo-list {
  list-style: none;
  margin-bottom: 20px;
}

.empty-state {
  text-align: center;
  padding: 60px 20px;
  color: #999;
  font-size: 18px;
}

/* Todo Item */
.todo-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px;
  margin-bottom: 12px;
  background: #fafafa;
  border-radius: 8px;
  transition: all 0.3s;
}

.todo-item:hover {
  background: #f0f0f0;
  transform: translateX(4px);
}

.todo-item.completed {
  opacity: 0.6;
}

.todo-item.completed .todo-text {
  text-decoration: line-through;
  color: #999;
}

.todo-check input[type="checkbox"] {
  width: 20px;
  height: 20px;
  cursor: pointer;
}

.todo-content {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 12px;
}

.todo-text {
  font-size: 16px;
  color: #333;
}

.todo-priority {
  font-size: 12px;
  white-space: nowrap;
}

.todo-actions button {
  padding: 6px 12px;
  font-size: 14px;
  background: #ff4d4f;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.3s;
}

.todo-actions button:hover:not(:disabled) {
  background: #ff7875;
}

.todo-actions button:disabled {
  background: #ccc;
  cursor: not-allowed;
}

/* Footer */
.footer {
  padding-top: 20px;
  border-top: 2px solid #f0f0f0;
}

.stats {
  display: flex;
  justify-content: space-around;
  margin-bottom: 20px;
  font-size: 16px;
  color: #666;
}

.stats strong {
  color: #667eea;
  font-size: 20px;
  margin-left: 5px;
}

.progress {
  text-align: center;
}

.progress-bar {
  width: 100%;
  height: 8px;
  background: #f0f0f0;
  border-radius: 4px;
  overflow: hidden;
  margin-bottom: 10px;
}

.progress-fill {
  height: 100%;
  background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
  transition: width 0.3s ease;
}

.progress-text {
  font-size: 14px;
  color: #999;
}
运行结果

📝 今日总结

关键知识点

  1. JSX = JavaScript + XML

    • {}嵌入JavaScript表达式
    • className代替classstyle用对象
    • 条件渲染用三元表达式或&&
    • 列表渲染用map(),必须提供key
  2. 函数组件

    • 组件名首字母大写
    • 通过props接收数据
    • props只读,不可修改
  3. Vue → React思维转换

    • 忘掉指令,拥抱JavaScript
    • 一切皆JavaScript表达式

完成检查清单

  • ✅ 创建React项目并理解项目结构
  • ✅ 掌握JSX基础语法
  • ✅ 理解函数组件和Props传递
  • ✅ 实现条件渲染(三元、逻辑与、提前return)
  • ✅ 实现列表渲染(map、key)
  • ✅ 完成待办事项静态版本

📚 课后作业

必做

  1. 完善待办事项样式

    • 调整颜色方案
    • 添加hover效果
    • 优化响应式布局
  2. 添加功能

    • 显示创建时间
    • 添加标签分类
    • 显示紧急程度图标

选做

  1. 创建新组件

    • 个人信息卡片(头像、姓名、简介)
    • 文章列表(标题、摘要、标签)
    • 天气卡片(城市、温度、图标)
  2. 思考题

    • 如果要让待办事项支持编辑,需要什么?
    • 如何实现拖拽排序?
    • 组件如何与后端API交互?

🚀 明天预告

Day 2: 状态管理与事件处理

明天我们将学习:

  • useState Hook - 让组件拥有自己的状态
  • 事件处理 - 响应用户操作
  • 表单处理 - 处理用户输入
  • 组件通信 - 父传子、子传父

完成后,我们的待办事项将真正"动起来"!


加油,訾博!第一天的学习辛苦了! 💪

相关推荐
AI智能研究院11 小时前
(四)从零学 React Props:数据传递 + 实战案例 + 避坑指南
前端·javascript·react.js
qq77982334011 小时前
React组件完全指南
前端·javascript·react.js
qq77982334012 小时前
React Hooks完全指南
前端·javascript·react.js
DoraBigHead12 小时前
React Fiber:从“递归地狱”到“时间切片”的重生之路
前端·javascript·react.js
HHHHHY14 小时前
使用阿里lowcode,封装SearchDropdown 搜索下拉组件
前端·react.js
KenXu16 小时前
TanStack Router 深度分析:与 React Router 的对比
react.js
浮游本尊17 小时前
React 18.x 学习计划 - 第五天:React状态管理
前端·学习·react.js
crary,记忆21 小时前
MFE: React + Angular 混合demo
前端·javascript·学习·react.js·angular·angular.js
一个处女座的程序猿O(∩_∩)O1 天前
React 多组件状态管理:从组件状态到全局状态管理全面指南
前端·react.js·前端框架
鹏多多1 天前
用useTransition解决React性能卡顿问题+实战例子
前端·javascript·react.js