第一个 React 应用

要点

这一篇在上一篇建好的 Vite 工程里,先用三十行跑通第一屏,再用一百行把它改成一个能增删、能勾选、刷新不丢数据的待办应用。第一课只有两句话:组件就是函数,JSX 是 JavaScript 的语法糖而不是模板语言。理解了这两句,React 的语法面比想象中小得多——剩下的都是 JavaScript。

从一屏开始

把 src/App.jsx 整个替换成:

// src/App.jsx
function App() {
  const name = 'React'
  return (
    <div>
      <h1>你好,{name}</h1>
      <p>现在是 {new Date().toLocaleDateString()}</p>
    </div>
  )
}

export default App

保存后页面自动热更新。逐条看 JSX 的规则,一共五条:

规则 例子 违反时的现象
组件名大写开头 function App() 而非 function app() 小写标签被当成原生 HTML 元素,渲染成空标签
最外层单根 两个兄弟元素要包一层 div 或空标签 <>…</> 编译直接报错,页面白屏,终端有明确行号
花括号里放表达式 {name}、{items.length}、{ok ? '是' : '否'} 放语句(if、for)会报错——表达式世界没有语句的位置
class 写成 className <p className="title"> class 能渲染但控制台告警,部分场景行为不一致
布尔与 null 不渲染 {false}、{null} 页面上什么都不出 数字 0 会渲染出来——条件渲染的经典坑,语法篇展开

JSX 会被 Vite 编译成普通的函数调用,所以你不需要 import React——这条从 React 17 起由新的 JSX 转换自动完成。”JSX 是糖”不是修辞:花括号里能放的东西,和你在一个 .js 文件里能写的东西完全一样。

状态:useState

静态页面不需要 React。让页面动起来的最小单位是 state:

import { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)
  // 数组解构出"当前值"与"设置器";0 是初始值,只在首次渲染用到
  return (
    <button onClick={() => setCount(count + 1)}>
      点了 {count} 次
    </button>
  )
}

关键机制一句话:调用 setCount 后,React 重新执行整个 App 函数,新一次渲染里的 count 才是新值。函数组件不是”执行一次的函数”,而是”React 反复调用的描述”——这句话是所有 React 心智的入口,本系列语法篇会反复用到它,完整的执行模型在站内《React Hooks 心智模型》深潜文里。

待办应用:完整代码

四步拼出来:列表、输入、增删改、拆组件。先看成品(可直接整文件粘贴):

// src/App.jsx
import { useState } from 'react'
import TodoItem from './TodoItem'

let nextId = 1   // 演示用自增 id;真实项目用 crypto.randomUUID()

function App() {
  const [todos, setTodos] = useState([])
  const [text, setText] = useState('')

  function addTodo(e) {
    e.preventDefault()               // 阻止表单默认提交导致的整页刷新
    const t = text.trim()
    if (!t) return                   // 空输入不入列——校验放在事件里,不放渲染里
    setTodos([...todos, { id: nextId++, text: t, done: false }])
    // 展开旧数组创建新数组:React 靠引用变化感知更新,直接 push 是反模式
    setText('')
  }

  function toggle(id) {
    setTodos(todos.map(t =>
      t.id === id ? { ...t, done: !t.done } : t   // 同理:换新对象,不改旧对象
    ))
  }

  function remove(id) {
    setTodos(todos.filter(t => t.id !== id))
  }

  return (
    <main>
      <h1>待办({todos.filter(t => !t.done).length} 件未完成)</h1>
      <form onSubmit={addTodo}>
        <input value={text} onChange={e => setText(e.target.value)} />
        <button>添加</button>
      </form>
      {todos.length === 0
        ? <p>还没有待办</p>
        : <ul>
            {todos.map(t => (
              <TodoItem key={t.id} todo={t}
                onToggle={toggle} onRemove={remove} />
            ))}
          </ul>}
    </main>
  )
}

export default App
// src/TodoItem.jsx —— 子组件只管"怎么画一条",数据与操作都从 props 来
function TodoItem({ todo, onToggle, onRemove }) {
  return (
    <li>
      <label>
        <input type="checkbox" checked={todo.done}
          onChange={() => onToggle(todo.id)} />
        <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
          {todo.text}
        </span>
      </label>
      <button onClick={() => onRemove(todo.id)}>删除</button>
    </li>
  )
}

export default TodoItem

四个知识点在代码里各就各位:

知识点 代码里的位置 一句话规则
受控输入 <input value={text} onChange={…}> value 与 onChange 成对出现;只给 value 不给 onChange,输入框打不了字
条件渲染 三元表达式切换空提示与列表 用 && 或三元,不用 if——JSX 里只能放表达式
列表与 key todos.map(…) 且 key={t.id} key 必须稳定唯一,用业务 id 不用数组下标——增删时下标会串位
子组件与 props TodoItem 接收 todo/onToggle/onRemove 父组件持有数据与操作,子组件只负责展示与回抛事件

刷新不丢:useEffect 第一口

渲染是纯计算,写 localStorage 这类副作用不能放在渲染过程里,归 useEffect 管。给 App 加两段:

import { useEffect } from 'react'

// 初始值改造:惰性初始化——useState 传入函数,首次渲染才执行一次读取
const [todos, setTodos] = useState(() => {
  try {
    return JSON.parse(localStorage.getItem('todos') ?? '[]')
  } catch {
    return []    // localStorage 被写坏时宁可从空开始,也别让整个应用崩掉
  }
})

// todos 变化后同步到 localStorage;依赖数组声明"盯谁"
useEffect(() => {
  localStorage.setItem('todos', JSON.stringify(todos))
}, [todos])

这是 useEffect 的最小用法:依赖数组 [todos] 表示 todos 变了就重跑。它为什么不是”生命周期钩子”、清理函数什么时候执行、漏依赖会踩什么雷——这些深入话题整篇都写在《React Hooks 心智模型》,第一周只需要照上面的形状用。

与 Vue 起步对照

同一个待办应用,Vue 3 的写法长什么样?站内《第一个 Vue 3 应用》是镜像教程,对照表放这儿:

维度 React Vue 3
声明状态 const [n, setN] = useState(0) const n = ref(0),改值即触发
写界面 JSX,花括号里是 JavaScript 模板 + 指令(v-if / v-for / v-model)
监听输入 受控:value + onChange 成对 v-model 一个指令双向绑定
列表渲染 todos.map(t => <li key={t.id}>) <li v-for="t in todos" :key="t.id">
副作用 useEffect + 手写依赖数组 watch 自动收集依赖

底层差别一句话:React 靠”状态变了就重新执行整个组件函数”,Vue 靠”依赖追踪、精确更新”。两边都学过一遍,你对”状态驱动视图”的理解会完整得多。

常见卡点

Q1: 页面白屏

九成是 JSX 结构问题:标签没闭合、最外层两个根。看终端与浏览器控制台,报错带行号,直接指向问题。

Q2: 输入框打不了字

value 绑了状态却没写 onChange——受控组件只有一半。React 不允许你在不提供更新路径的情况下锁定 value。

Q3: 控制台警告 Each child in a list should have a unique key

map 渲染的列表没给 key。用稳定 id;用随机数也不行——每次渲染都是新值,React 无法对上节点。下标当 key 在增删场景会串位,语法篇有专门展开。

Q4: 勾选了没反应

八成是直接改了对象:todo.done = true。要创建新对象 { ...t, done: !t.done }——React 比较的是引用,旧对象原地改它感知不到。

反模式

反模式一:直接修改状态。todos.push(…) 之后界面不动,因为数组引用没变。所有更新都是”创建新的替换旧的”:展开、map、filter 三个高阶操作覆盖九成场景。

反模式二:把逻辑堆回 App.jsx 不拆组件。TodoItem 拆出来不是为了目录好看——列表项的渲染细节收进子组件后,App 里只剩数据流,这是后面所有复杂度的收纳方式。

检查清单

  • 第一屏渲染出来,能对 JSX 五条规则各举一个例子
  • 待办应用:添加、勾选、删除、空列表提示全部可用
  • 输入框是受控的:value 与 onChange 成对
  • 列表 key 用的是 id,控制台无 key 警告
  • 刷新页面数据还在;手动把 localStorage 改成非法 JSON 再刷新,应用不崩
  • 能口头回答:setTodos 之后 React 做了什么

下一步

这一篇走完了”组件—状态—子组件—副作用”的最小闭环。下一篇《React 语法要点:组件、JSX 与数据流》把 props 单向流动、children、组合与继承的取舍这些”为什么”讲透;useEffect 想深入的现在就可以插读上节链接的《React Hooks 心智模型》,与本篇的”第一口”正好衔接。

版本核实(撰写时检索,2025-07):React 当前稳定版为 19.1.x;自动 JSX 转换(无需 import React)自 React 17 起可用;crypto.randomUUID 为浏览器原生 API;本篇全部 API(useState/useEffect)为 React 稳定特性。具体以官方文档与你的锁文件为准。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇