React 语法要点:组件、JSX 与数据流

要点

React 的”语法”薄得惊人:组件、JSX、props、Hooks,四个概念讲完,剩下全是 JavaScript。这一篇按数据流走一遍:props 怎么单向流动、children 怎么把组装权交回调用方、条件与列表渲染的坑、受控组件的原理、以及 React 为什么用组合彻底拒绝继承。渲染机制的深水区(什么时候重渲染、怎么避免白算)不在本篇——那是深潜文的地盘,文内随用随链。

props:单向数据流是纪律,不是限制

props 是组件的输入契约:父组件传什么,子组件拿什么,子组件对 props 只有读的份。数据只能从上往下流,子组件想改数据,只有一个办法——调用父组件通过 props 传进来的回调:

// 父组件:持有数据,也持有改数据的操作
function TodoList() {
  const [todos, setTodos] = useState([])
  function toggle(id) {
    setTodos(todos.map(t => (t.id === id ? { ...t, done: !t.done } : t)))
  }
  return todos.map(t => (
    <TodoItem key={t.id} todo={t} onToggle={toggle} />
  ))
}

// 子组件:数据怎么画我说了算,数据本身我碰不了
function TodoItem({ todo, onToggle }) {
  return (
    <li onClick={() => onToggle(todo.id)}>
      {todo.done ? '✓ ' : ''}{todo.text}
    </li>
  )
}

为什么要这么绕?因为单向流动让”谁改了数据”永远只有一个答案:数据的持有者。反面对照是双向绑定随手改——bug 出现时你要在全项目里找”哪个组件偷偷改了它”。React 把改数据的路径收窄成显式的回调链,代价是样板代码,收益是可追踪。

props 里传的不只是数据:函数(回调)、组件本身、任何 JavaScript 值都行。默认值用解构参数给:function Badge({ tone = 'gray' })。

children:组装权交回调用方

每个组件都自动有一个 props.children——标签之间包着的内容:

// Card 只定义骨架,不关心里面放什么
function Card({ title, children }) {
  return (
    <section className="card">
      <header>{title}</header>
      <div className="body">{children}</div>
    </section>
  )
}

// 调用方决定内容——这就是组合(composition)
<Card title="今日">
  <p>任何组件、任何 JSX 都能放进来</p>
</Card>

children 是 React 解决”内容分发”的原生答案,对应 Vue 的插槽。具名插槽的等价物是”传组件当 props”:<Layout sidebar={<Menu />} main={<Article />} />——不需要框架发明新语法,因为 JSX 里组件本来就是值。

条件渲染:三种形态与一个经典坑

// 形态一:&& —— 有就渲染,没有就算了
{hasError && <p className="error">出错了</p>}

// 形态二:三元 —— 二选一
{todos.length > 0 ? <List items={todos} /> : <p>暂无数据</p>}

// 形态三:提前 return —— 分支复杂时把渲染逻辑拆段
function Panel({ user }) {
  if (!user) return <p>请先登录</p>
  return <div>{user.name} 的面板</div>
}

经典坑在形态一:JSX 里布尔与 null 不渲染,但数字 0 会。{count && <Badge />} 在 count 为 0 时会把一个孤零零的 0 渲染到页面上。写法上防死:{count > 0 && <Badge />}。

列表渲染:key 是给 React 的对账凭据

{todos.map(t => (
  <TodoItem key={t.id} todo={t} />
))}

map 把数组变成元素数组,key 是其中每项的身份标识。React 更新列表时靠 key 判断”哪个节点还是原来那个”——勾选状态、输入框内容这些挂在节点上的东西,跟着 key 走而不是跟着位置走。

用数组下标当 key 的后果:头部插入或删除一项后,所有下标错位,React 把第 n 个节点当成还是原来的第 n 个,节点的本地状态与数据对不上号。数据没有 id 就造一个稳定的(crypto.randomUUID()),随机数每次渲染都变,等于没有 key。这一机制与 React 的 diff 过程直接相关,想看渲染时到底发生了什么,接《React 渲染与重渲染》。

受控组件:表单值住进 state

HTML 表单控件自带内部状态(输入框自己记自己的值),React 的做法是把这个状态收编——value 由 state 提供,变更由 onChange 写回,形成闭环:

const [text, setText] = useState('')

<input value={text} onChange={e => setText(e.target.value)} />

// textarea 与 select 是同一套:都是 value + onChange
<textarea value={text} onChange={e => setText(e.target.value)} />
<select value={lang} onChange={e => setLang(e.target.value)}>
  <option value="zh">中文</option>
  <option value="en">English</option>
</select>

受控的价值:值永远在 state 里,校验、禁用、联动、提交前处理都在数据层完成,DOM 只是投影。代价是每次按键触发一次渲染——输入规模下这个开销无感,真遇到性能问题时的出路在渲染篇。非受控写法(defaultValue + ref 直取 DOM 值)留给文件输入这类无法受控的场景,不作默认。

状态提升:共享数据的唯一正路

两个组件要读写同一份数据时,把这份数据提到它们最近的公共父组件里,通过 props 分发下去。比如”筛选输入框”与”待办列表”都要用关键词:

function FilterableList() {
  const [keyword, setKeyword] = useState('')   // 状态住在公共父组件
  const filtered = todos.filter(t => t.text.includes(keyword))
  return (
    <>
      <SearchInput value={keyword} onChange={setKeyword} />
      <TodoList items={filtered} />
    </>
  )
}

找状态的居住地有句口诀:谁用,住谁那;都用,住最近公共父组件那。再往上,当”最近公共父组件”变成整个应用时,才轮到 Context 或状态库出场——先把提升用透,才知道那些工具在替你解决什么。

组合优于继承:React 的明确立场

React 没有组件继承。官方文档的态度很直接:他们没遇到过任何必须用继承才能解决的 UI 复用问题。复用的两把工具都是组合:

复用诉求 组合手法 继承思维会怎么写(反面)
外壳相同内容不同 children(上文 Card) 写 CardBase 再 extends 出 CardWithTitle
行为相同数据不同 props 传数据 + 回调 基类持有数据,子类覆盖渲染方法
逻辑复用 自定义 Hook(下一节) 基类里的公共方法,子类调用

继承的问题在 React 语境里很具体:组件是”被 React 反复调用的函数”,继承链会把”哪些层改了什么状态”藏进调用栈,与单向数据流的可见性直接冲突。

自定义 Hook 基础:复用逻辑,不复用状态

自定义 Hook 就是名字以 use 开头、内部调用了其他 Hook 的函数。它复用的是”逻辑的形状”,每次调用各自持有独立状态:

// useLocalStorage:把"state + 持久化"打包成一个可复用单位
import { useState, useEffect } from 'react'

export function useLocalStorage(key, initial) {
  const [value, setValue] = useState(() => {
    try {
      const raw = localStorage.getItem(key)
      return raw !== null ? JSON.parse(raw) : initial
    } catch {
      return initial    // 坏数据宁可回初始值——持久化层不能带崩应用
    }
  })

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value))
  }, [key, value])      // value 变了才写盘;key 变了也要重写

  return [value, setValue]
}

// 用起来与 useState 无异,但多了持久化
const [theme, setTheme] = useLocalStorage('theme', 'light')

两条铁律(违反会导致渲染错乱,ESLint 的 rules-of-hooks 规则会拦):只在函数顶层调用 Hook,不在条件、循环、嵌套函数里调;只在 React 函数组件或自定义 Hook 里调用。use 前缀同时是给 lint 和给读者的约定——看到 use 就知道它参与渲染周期。

两个组件分别调用 useLocalStorage(‘a’) 与 useLocalStorage(‘b’),拿到的是两份互不影响的状态——”复用逻辑不复用状态”。要共享状态,回到状态提升,或 Context。Hook 的执行语义(闭包快照、依赖数组背后的机制)在《React Hooks 心智模型》整篇展开。

反模式

反模式一:在子组件里改 props。props.todo.done = true 改的是父组件对象的嵌套字段,看似生效,实则父组件毫无感知,下一次渲染就被覆盖,还伴随告警。改数据的路径只有一条:回调给数据的持有者。

反模式二:props 层层透传当默认操作。中间组件既不用某数据又原样转发(props drilling),三层之后每加一个字段要改三个文件。先用组合把”需要数据的组件”直接变成 children 或 props 传进去;确实全局的语义(主题、当前用户)再上 Context——但记住 Context 变化会让所有消费者重渲染,粒度设计见渲染篇。

检查清单

  • 能对”单向数据流”说出子组件改数据的唯一合法路径
  • children 与”传组件当 props”两种组合手法各写过一个例子
  • 知道 {count && …} 在 count 为 0 时渲染什么,以及怎么防
  • 能解释 index 当 key 在什么操作下串位、为什么随机数 key 也不行
  • 受控组件的闭环(value ← state,onChange → setState)画得出来
  • useLocalStorage 例子能独立重写,两条 Hook 铁律背得出

下一步

这一篇讲完”为什么”,全部 API 的签名、示例与坑集中在《React 基础知识参考字典》随查随用;接下来《用 React + Vite 做一个热榜阅读客户端》把这些语法放进一个真实项目,补上数据层与持久化的工程决策。

版本核实(撰写时检索,2025-07):React 当前稳定版为 19.1.x;本篇涉及的 props/children/key/受控组件/自定义 Hook 均为 React 长期稳定特性,无版本敏感 API;React 19 起函数组件的 ref 作为普通 prop 传递(本篇未涉及)。具体以官方文档为准。

暂无评论

发送评论 编辑评论


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