Skip to content

组件通信与 Hooks(一) ​

本页对应 组件通信与 Hooks,共 7 段视频,约 1 小时 40 分。

学习目标 ​

  • 能够使用 useState 配合 value/onChange(复选框使用 checked)实现受控表单组件,并说明 value 与 onChange 需同时出现的原因及缺失时的表现。
  • 能够使用 useRef 创建并绑定 DOM 引用,通过 ref.current 完成获取输入值、聚焦等 DOM 操作,并避免在组件渲染阶段进行 DOM 操作。
  • 能够实现 B 站评论发布案例:读取受控评论内容、组装评论数据、按“最新/最热”排序更新列表、发布后清空输入框,并在内容为空时聚焦输入框。
  • 能够通过 props 向组件传递数据并在组件中接收和解构使用,正确使用花括号传递非字符串类型数据,利用解构默认值设置 props 默认值,并说明 props 只读特性。
  • 能够实现父子组件通讯:父组件通过 props 向子组件传数据,子组件通过调用父组件传入的回调函数回传数据,并遵循“谁的数据谁负责”的原则完成状态更新。
  • 能够使用状态提升实现兄弟组件通讯,并使用 createContext、Provider、useContext 完成跨层级组件通讯,共享颜色状态与重置函数并实现双向更新。

知识点 ​

先想清楚:为什么要用状态管表单 ​

React 是「数据驱动视图」:页面要变,就得有状态变。但表单元素是个特殊存在——用户在输入框里敲字,页面内容自己就变了,这条路绕过了状态。于是同一份数据出现两个源头:状态里一份、DOM 里一份,谁也不服谁。所以要把表单元素的值「交」给状态托管:状态是唯一真相,DOM 只是它的投影。

思路分两步:先准备状态,再把状态和表单元素双向接起来——一个方向数据到视图,一个方向视图到数据。很像 Vue 的双向绑定,但 React 并没有双向绑定,我们只是用两个单向动作拼出了这个效果。

受控文本框:value + onChange ​

jsx
const [value, setValue] = useState('') // 文本框给空字符串

<input
  value={value}                                  // 数据 → 视图
  onChange={(e) => setValue(e.target.value)}     // 视图 → 数据
/>

第一行把状态的 value 塞给 input 的 value 属性;第二行在 change 里通过 e.target.value 拿到最新输入,再 setValue 回写状态。两头接上,状态和输入框就绑成一体了:以后想改输入框,改状态就行。

验证方式很直接:给「获取」按钮绑点击事件,打印 value;给「修改」按钮绑点击事件,setValue('黑马')。点获取能拿到输入的内容,点修改输入框内容跟着变——说明确实绑上了。

换成 checkbox:属性换成 checked ​

复选框只有选中/不选中两种值,状态就该是布尔值:

jsx
const [checked, setChecked] = useState(false)

<input
  type="checkbox"
  checked={checked}
  onChange={(e) => setChecked(e.target.checked)}
/>

注意两个细节:绑定的属性不是 value 而是 checked;事件里取的也不是 e.target.value,而是 e.target.checked。页面里写个三元判断 checked ? '选中了' : '未选中',勾一下就能看到状态带着视图一起变。

易错点:value 和 onChange 必须成对出现 ​

只写 value 不写 onChange,控制台会警告「提供了 value 却没提供 onChange」,而且输入框根本敲不进字——因为值被状态锁死了,又没有回写通道。补上 onChange 后警告消失、输入恢复正常。记住这条:受控表单元素的两个属性是一对,缺一不可。

另外两点值得记:一是这套「受状态控制的表单元素」有个专业术语叫受控组件;二是 React 文本框的 change 事件更接近原生 input 事件,每次输入都触发,而原生 change 是失焦才触发——这么做是为了统一,不管 input、checkbox、textarea 还是 select,统统用 change 一个事件处理。

useRef:偶尔确实要碰 DOM ​

有些事状态办不到,比如让输入框获得焦点。用法两步:const inputRef = useRef(null) 创建 ref 对象(此时还没拿到 DOM,所以默认值给 null),再把它挂到元素的 ref 属性上完成绑定。之后通过 inputRef.current 才拿到真实 DOM,再 .value、.focus()。

关键易错点:不要在组件渲染过程中做 DOM 操作。组件代码从上往下执行,useRef 刚创建时 JSX 还没渲染,inputRef.current 就是 null,此时点 value 会报「Cannot read properties of null」。只有在事件回调里(渲染已完成)才能拿到真实 DOM。还有一条建议:操作输入框的值,优先用受控组件,那才符合数据驱动视图的思想;ref 只用来做 DOM 层面的事。

发布评论:受控 + 排序 + 聚焦 ​

评论框是 textarea,同样走受控路线(value / onChange),拿到状态就能直接当评论内容用。点击发布时:组装一条评论数据(id 用时间戳,user 用当前用户,content 用状态 value,时间用 dayjs 格式化成「月日时分」,点赞数 0,action 0)→ 基于旧 list 创建新数组 [comment, ...list] → 根据 activeType 判断排序依据(最新按 ctime、最热按 like,都降序)→ setList(排序结果)。

两个坑:一是排序必须对新数组排,不是对旧 list;二是不能直接改 list,要创建新数据。最后别忘了 setValue('') 清空评论框——框已受控,清状态就等于清输入框。

还有个体验细节:内容为空时不允许发布,改为让评论框获得焦点。这就是前面 ref 的用武之地:value.trim() 为空就 return textRef.current.focus(),等于提醒用户「先写点东西」。

props:组件之间传数据 ​

组件要复用,图片地址就不能写死。传:直接在标签上写属性 <Avatar imageUrl={url} />,命名用小驼峰。收:函数参数就是 props 对象,function Avatar(props) 里 props.imageUrl 即可;推荐直接解构 function Avatar({ imageUrl, size = 50 }),更简洁,还能用解构默认值兜底。传非字符串(数字、布尔、函数)必须用花括号,否则拿到的是字符串。props 是只读的,直接赋值会报「Cannot assign to read only property」,想改数据只能改数据源。

父子通讯与状态提升 ​

父到子就是传 props,还可以用 <Todo {...item} /> 把整个对象展开。子到父要守住一个原则:谁的数据谁负责。数据在父组件,父组件就准备好修改函数 onToggle(id) 传给子组件,子组件在点击时调用并回传 id,父组件用 map 生成新数组、翻转 done、setTodo。

兄弟组件不能直接通讯,要找公共父组件当中间人:把共享数据(选中的好友)提升到父组件,用数据的走父到子(props),改数据的走子到父(回调),父组件数据一变,另一个兄弟自动更新——这叫状态提升。

层级更深的用 Context:createContext() 创建 → Context.Provider value={共享数据} 划定范围 → 范围内任意层级 useContext(Context) 取值。注意 Provider 的 value 属性是必须的,漏写直接报错。共享对象和函数都行,于是父改子、子改父的双向通讯都能实现。

为什么需要 useRef:从“数据驱动”说到必须碰 DOM 的场景 ​

React 的核心是数据驱动视图:想改变页面内容,优先改状态,让 React 重新渲染。但有些操作天生更接近 DOM 本身,例如让输入框获得焦点、读取某个 DOM 节点的值、滚动到某个位置等。Vue 中常用 ref 属性拿 DOM,React 中也有类似思路,工具就是 useRef。

这里要先建立一个判断:能通过状态完成的,优先用状态。转写稿里也特别强调,操作文本框的值更推荐“受控组件”,因为那更符合 React 数据驱动视图的思想。useRef 更适合处理“必须直接操作 DOM”的场景,比如评论框为空时调用 focus() 提醒用户输入。

怎么想:创建 ref、绑定 ref、读 current ​

使用 useRef 做 DOM 操作可以按两步想:

  1. 创建一个 ref 对象,并和 JSX 元素绑定;
  2. 通过 ref.current 拿到真实 DOM,再做 DOM 操作。

创建时通常写成 useRef(null)。为什么参数是 null?因为创建 ref 对象时,组件还没渲染,DOM 元素还没出现,此时还没有真实 DOM 可拿,所以先用 null 表示“尚未绑定”。绑定方式是在目标元素上写 ref 属性,值是刚创建好的 ref 对象。绑定完成后,将来就可以通过 ref.current 访问这个 DOM 元素。

怎么做:获取输入框值与让输入框获得焦点 ​

下面用输入框和两个按钮演示:一个按钮获取输入框的值,一个按钮让输入框获得焦点。

jsx
import { useRef } from 'react';

function App() {
  const inputRef = useRef(null);

  const getValue = () => {
    console.log(inputRef.current.value);
  };

  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} />
      <button onClick={getValue}>获取值</button>
      <button onClick={focusInput}>获得焦点</button>
    </div>
  );
}

关键点是 inputRef.current。inputRef 本身只是 ref 对象,真正 DOM 在 current 里。拿到 DOM 后,.value 可以读输入框的值,.focus() 可以让输入框获得焦点。转写稿的调试过程也是先打印 inputRef.current,看到输入框对应的 DOM 元素,再点 .value 或 .focus() 验证效果。

调试过程:先确认 current 是不是 DOM,再操作 ​

调试时不要一上来就写复杂逻辑,可以先打印:

jsx
console.log(inputRef.current);

如果是在点击事件里打印,通常能看到真实 DOM 元素;接着再打印 inputRef.current.value,输入内容后点击按钮,能拿到最新值;再调用 inputRef.current.focus(),输入框会自动获得焦点。这个过程能帮助确认“ref 是否绑定成功”和“current 是否拿到 DOM”。

易错点:渲染阶段不要操作 DOM,别漏 current ​

第一个易错点是忘记 current。inputRef.value 不是 DOM 的 value,必须写 inputRef.current.value。

第二个易错点是不要在组件渲染过程中直接做 DOM 操作。转写稿里演示过:在组件函数体里打印 inputRef.current,结果是 null。因为组件从上往下执行时,JSX 还没渲染完,ref 还没和 DOM 绑定;此时再去点 .value 或 .focus(),就会报“读取 null 的属性”这类错误。正确做法是把 DOM 操作放到事件回调里,例如点击按钮时再执行。

第三个易错点是滥用 useRef 操作表单值。虽然通过 ref.current.value 也能拿到文本框的值,但 React 更推荐用状态和 onChange 实现受控组件。useRef 更适合焦点、滚动、测量等 DOM 场景。把这一点记住,就不会把“能操作 DOM”误当成“应该操作 DOM”。

本节目标与整体思路 ​

这一节要在 B 站评论案例里完成“发布评论”:输入评论内容,点击发布后把新评论加到列表里,并按当前排序规则显示;如果内容为空,则不发布,并让评论框获得焦点。

为什么需要先理清流程?因为发布评论同时涉及表单值、列表状态、排序和 DOM 操作。怎么想?先把评论内容变成 React 状态,再用受控组件拿到它;发布时组装评论对象,创建新数组,排序后更新 list;最后清空评论框。怎么做?按“拿内容 → 组装数据 → 加入列表 → 排序 → 更新状态 → 清空”推进。易错点:不要直接改原数组,不要忘记排序,不要忘记清空。

用受控组件拿到评论内容 ​

为什么需要受控组件?React 是数据驱动视图,要操作表单元素的值,就要让表单值受状态控制。怎么想?评论框是 textarea,它和 input 一样是表单元素,可以用 value + onChange 模拟双向绑定。怎么做?

jsx
const [value, setValue] = useState('')

<textarea
  value={value}
  onChange={(e) => setValue(e.target.value)}
/>

调试时可以先给发布按钮绑定点击事件,打印 value,输入“黑马”后点击发布,能打印出内容,说明评论框已经和状态绑定。易错点:value 和 onChange 要同时出现;只写 value 会出现警告,而且输入不进去。React 中这叫“受控组件”。

发布评论:组装、插入、排序、更新 ​

为什么不能直接改 list?React 更新状态要基于现有数据创建新数据。怎么想?点击发布时,先判断有没有内容;有内容就组装一条评论,再把它加到新数组里,根据 activeType 排序,最后 setList。怎么做?

jsx
const comment = {
  pid: Date.now(),
  user,
  content: value,
  ctime: dayjs().format('MM-DD HH:mm'), // 月日时分
  like: 0,
  action: 0
}

const newList = [comment, ...list]

let sortedList
if (activeType === 'time') {
  sortedList = orderBy(newList, 'ctime', 'DESC')
} else {
  sortedList = orderBy(newList, 'like', 'DESC')
}

setList(sortedList)

调试时:按“最新”排序,新评论应出现在第一条;按“最热”排序,新评论 like 为 0,应排在最后。易错点:排序要排 newList,不是旧 list;不要 list.push;ctime 用 dayjs 生成,like 默认 0。

发布后清空评论框 ​

为什么需要清空?发布后评论框还保留内容,用户体验不好。怎么想?评论框已经受 value 控制,清空表单值就是清空状态。怎么做?发布成功后调用 setValue('')。易错点:不要直接操作 DOM 改 textarea.value,优先改状态。

空内容时让评论框获得焦点 ​

为什么需要?没写内容就点发布,不能发布,但要让用户知道该写内容。怎么想?这是 DOM 操作,用 useRef 创建 textRef,绑定到 textarea 的 ref 属性,通过 textRef.current 拿到 DOM 后调用 focus()。怎么做?

jsx
const textRef = useRef(null)

<textarea ref={textRef} value={value} onChange={...} />

if (!value.trim()) {
  textRef.current.focus()
  return
}

易错点:不要在组件渲染过程中做 DOM 操作,渲染时 ref.current 可能还是 null;应在点击事件里操作。判断空内容时先 trim() 去掉两端空格。

易错点小结 ​

value 与 onChange 要成对出现;发布评论要创建新数组再 setList;排序对象应是加入新评论后的 newList;发布成功后 setValue('');空内容时 return 并 focus();textarea 也是受控组件,操作它优先改状态。

代码示例 ​

示例 1:使用状态操作表单元素的值(受控组件) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>受控组件</title>
</head>
<body>
  <div id="root"></div>

  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

  <script type="text/babel">
    const { useState } = React

    function App() {
      // 第一步:准备状态(文本框给字符串,复选框给布尔值)
      const [value, setValue] = useState('')
      const [checked, setChecked] = useState(false)

      return (
        <div style={{ padding: 20 }}>
          {/* 文本框:value + onChange 必须同时出现 */}
          <input
            value={value}
            onChange={(e) => setValue(e.target.value)}
          />
          <button onClick={() => console.log(value)}>获取</button>
          <button onClick={() => setValue('黑马')}>修改</button>

          <hr />

          {/* 复选框:绑定的属性是 checked,不是 value */}
          <input
            type="checkbox"
            checked={checked}
            onChange={(e) => setChecked(e.target.checked)}
          />
          <span>{checked ? '选中了' : '未选中'}</span>
        </div>
      )
    }

    ReactDOM.createRoot(document.getElementById('root')).render(<App />)
  </script>
</body>
</html>

要点解释:

  • useState 准备状态:文本框给空字符串,复选框给 false(布尔值),状态类型要和表单元素的值类型对上。
  • 模拟双向绑定:value={value} 是数据到视图,onChange 里 setValue(e.target.value) 是视图到数据,两者缺一不可。
  • 易错点:只写 value 不写 onChange,React 会警告 You provided a value prop to a form field without an onChange handler,而且输入框写不进任何内容。
  • 易错点:复选框要读 e.target.checked(选中状态),不是 e.target.value。
  • React 的 onChange 更接近原生 input 事件,每输入一个字符就触发一次(原生 change 是失焦才触发),好处是 input / checkbox / textarea / select 都用同一个事件。
  • 被状态控制住的表单元素叫「受控组件」,之后操作输入框只要操作状态即可。

示例 2:useRef 与 DOM 操作 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>useRef 与 DOM 操作</title>
</head>
<body>
  <div id="root"></div>

  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

  <script type="text/babel">
    const { useRef } = React

    function App() {
      // 第一步:创建 ref 对象,默认值给 null(此时还没有 DOM)
      const inputRef = useRef(null)

      // 易错点:不要在渲染过程中拿 ref 做 DOM 操作
      // 这里打印出来是 null,因为 JSX 还没渲染,ref 还没绑定 DOM
      // console.log(inputRef.current)  // null

      return (
        <div style={{ padding: 20 }}>
          {/* 第二步:用 ref 属性把 ref 对象和 DOM 绑定 */}
          <input ref={inputRef} />

          {/* 事件里操作,此时组件已渲染完成,current 才是真实 DOM */}
          <button onClick={() => console.log(inputRef.current.value)}>
            获取
          </button>
          <button onClick={() => inputRef.current.focus()}>
            获得焦点
          </button>
        </div>
      )
    }

    ReactDOM.createRoot(document.getElementById('root')).render(<App />)
  </script>
</body>
</html>

要点解释:

  • useRef(null) 的参数是初始值,因为创建 ref 时还没拿到 DOM,所以给 null。
  • ref={inputRef} 把 ref 对象和 DOM 元素绑定,绑定后通过 inputRef.current 才能拿到 DOM。
  • 易错点:直接写 inputRef.value 是拿不到的,必须 inputRef.current.value。
  • 易错点:不要在组件渲染过程中(函数体里)使用 ref 做 DOM 操作,此时 current 还是 null,会报 Cannot read properties of null (reading 'value');要放到事件回调里,等渲染完成后再用。
  • 操作输入框的值仍推荐用受控组件(状态),ref 主要用于 focus() 这类纯 DOM 操作。

示例 3:B 站评论案例 —— 发布评论 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>B站评论-发布评论</title>
</head>
<body>
  <div id="root"></div>

  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  <script src="https://unpkg.com/lodash@4.17.21/lodash.min.js"></script>
  <script src="https://unpkg.com/dayjs@1.11.10/dayjs.min.js"></script>

  <script type="text/babel">
    const { useState, useRef } = React

    // 当前登录用户
    const user = { name: '黑马程序员', avatar: 'https://picsum.photos/40' }

    // 初始评论列表(每条评论的数据结构)
    const defaultList = [
      { rpid: 1, user: { name: '小明' }, content: '讲得真好', ctime: '10-01 09:12', like: 30, action: 0 },
      { rpid: 2, user: { name: '小红' }, content: '学到了', ctime: '10-02 15:30', like: 8, action: 0 },
    ]

    function App() {
      const [list, setList] = useState(defaultList)
      const [value, setValue] = useState('')            // 受控组件:评论框内容
      const [activeType, setActiveType] = useState('hot') // 'hot' 最热 | 'time' 最新
      const textRef = useRef(null)                      // 评论框的 ref

      const onSend = () => {
        // 没有内容:让评论框获得焦点,提示用户去写,然后终止
        if (value.trim() === '') {
          textRef.current.focus()
          return
        }

        // 1. 组装一条评论数据
        const comment = {
          rpid: Date.now(),                 // 用时间戳当 id
          user,                             // 当前登录用户
          content: value,                   // 评论内容取状态
          ctime: dayjs().format('MM-DD HH:mm'),
          like: 0,
          action: 0,
        }

        // 2. 创建新数组(不要直接改 state)
        const newList = [comment, ...list]

        // 3. 根据 activeType 排序,排序后更新状态
        if (activeType === 'time') {
          setList(_.orderBy(newList, 'ctime', 'desc'))
        } else {
          setList(_.orderBy(newList, 'like', 'desc'))
        }

        // 4. 清空评论框:操作状态即可
        setValue('')
      }

      const changeTab = (type) => {
        setActiveType(type)
        if (type === 'time') {
          setList(_.orderBy(list, 'ctime', 'desc'))
        } else {
          setList(_.orderBy(list, 'like', 'desc'))
        }
      }

      return (
        <div style={{ width: 420, padding: 20 }}>
          <textarea
            ref={textRef}
            value={value}
            onChange={(e) => setValue(e.target.value)}
            placeholder="发一条友善的评论"
            style={{ width: '100%', height: 80 }}
          />
          <button onClick={onSend}>发布</button>
          <button onClick={() => changeTab('hot')}>最热</button>
          <button onClick={() => changeTab('time')}>最新</button>

          <ul>
            {list.map((item) => (
              <li key={item.rpid} style={{ margin: '10px 0' }}>
                <b>{item.user.name}</b>
                <span style={{ color: '#999', marginLeft: 8 }}>{item.ctime}</span>
                <p style={{ margin: '4px 0' }}>{item.content}</p>
                <span>👍 {item.like}</span>
              </li>
            ))}
          </ul>
        </div>
      )
    }

    ReactDOM.createRoot(document.getElementById('root')).render(<App />)
  </script>
</body>
</html>

要点解释:

  • 获取评论内容用受控组件:useState('') + value + onChange,之后拿评论框内容直接拿 value 状态即可。
  • 空内容判断用 value.trim() === '' 去掉两端空格,为空时 textRef.current.focus() 并 return,这是纯 DOM 操作,所以用 useRef。
  • 易错点:改列表状态不要直接 list.push(...) / list.unshift(...),要基于现有数据创建新数组 const newList = [comment, ...list]。
  • 易错点:排序必须对新数组 newList 排序,不能对旧的 list 排,否则新评论没参与排序。
  • 排序用 _.orderBy(数组, '排序字段', 'desc'):最新按 ctime,最热按 like,依据 activeType 判断;排序结果直接交给 setList。
  • 发布后评论框内容还在,需要 setValue('') 清空,因为评论框已经和状态绑定,清空状态就等于清空输入框。
  • 列表渲染的 key 用 rpid,保证每条评论的唯一标识。

常见错误 ​

  1. 输入框敲不进字,控制台报 "You provided a value prop to a form field without an onChange handler"

    • 原因:只写了 value={state},把输入框的值锁死在状态上,却没有从视图回写状态的通道。
    • 修法:补上 onChange={e => setValue(e.target.value)};若确实只要初始值不要受控,改用 defaultValue。
  2. 复选框勾选后状态不跟着变,或初始选中状态失效

    • 原因:复选框照抄文本框写成 value={checked},但复选框表达"选中与否"的属性是 checked,取值来自 e.target.checked 而非 e.target.value。
    • 修法:checked={checked} + onChange={e => setChecked(e.target.checked)}。
  3. ref.current 打印出来是 null,接着 .value/.focus() 直接报 Cannot read properties of null

    • 原因:在组件函数体(渲染阶段)里就访问 ref,此时 JSX 还没挂载;或者忘了在标签上写 ref={inputRef},只创建了 ref 对象却没绑定;或者错写成 ref={inputRef.current}。
    • 修法:把 DOM 操作挪进事件回调或 useEffect;标签上写 ref={inputRef}(传对象本身)。
  4. 给组件传数字/布尔/函数,子组件拿到的却是字符串

    • 原因:写成 size="100",引号里的内容一律按字符串传递。
    • 修法:非字符串一律加花括号 size={100}、disabled={true}、onClick={handleClick}。
  5. 子组件里改 props 报 Cannot assign to read only property,或父组件数据被"偷偷"改掉后视图不更新

    • 原因:props 只读;或子组件直接 list.push(...)、item.done = !item.done 原地修改父组件数据,引用没变 React 不重渲染。
    • 修法:遵守"谁的数据谁负责"——父组件提供修改函数并通过 props 下传,子组件只负责调用并回传数据;父组件内用 map 生成新对象、[...list, newItem] 生成新数组后再 setState。
  6. 使用 Context 时页面白屏报 "The value prop is required"

    • 原因:<XxxContext.Provider> 漏写了 value 属性。
    • 修法:<XxxContext.Provider value=&#123;&#123;...&#125;&#125;> 必须显式提供;需要共享多个数据时用对象包一层,后续扩展不用改结构。
  7. 兄弟组件想直接互传数据,或子组件里想直接拿兄弟组件的状态

    • 原因:组件是独立的,没有公共父级做中转就无法直接通讯。
    • 修法:把共享数据"提升"到最近的公共父组件,一方用回调改、一方用 props 读;层级过深再改用 Context。

练习 ​

  1. 受控搜索框:实现一个搜索框,输入时实时过滤下方列表(keyword 状态 + filter 计算),带一个"清空"按钮把关键字重置为空,并让搜索框重新获得焦点。要求:过滤逻辑不要写进 onChange 里做副作用,只让状态变化驱动结果。

  2. 弹窗自动聚焦:做一个点击按钮弹出的输入弹窗,要求弹窗出现后输入框自动获得焦点;弹窗关闭再打开时依然生效。提示:用 useRef + useEffect,思考为什么不能直接写在函数体里。

  3. props 改造:把现有写死的"商品卡片"组件改成完全由 props 驱动(图片地址、标题、价格、是否包邮、点击加入购物车的回调函数),并给价格、是否包邮设默认值。再在父组件里渲染 3 张卡片,验证传值类型正确。

  4. 待办列表删除:在父子组件通讯案例基础上实现"点击删除按钮移除对应任务"。要求子组件只负责调用父组件下传的函数并回传 id,删除逻辑(filter 生成新数组 + setState)全部写在父组件中。

  5. 主题切换升级:把主题颜色案例中的三层菜单改成"菜单 → 菜单组 → 菜单项"四级结构,并新增一个"随机主题"按钮放在最深层的菜单项里。要求不逐层传 props,直接用 Context 实现读颜色、写颜色两个方向的通讯。

课堂画面 ​

课堂画面 01

课堂画面 02

课堂画面 03

课堂画面 04

课堂画面 05

课堂画面 06

原视频对照 ​

#视频时长
1使用状态操作表单元素的值08:35
2useRef与DOM操作07:55
3B站评论案例-发布评论18:25
4组件的props14:38
5组件通讯-1父子组件通讯16:23
6非父子组件通讯-1兄弟组件通讯17:45
7非父子组件通讯-2跨组件通讯17:10

小结 ​

  1. React 没有双向绑定,"受控"的本质是 value + onChange 两条线缺一不可;只写一条线的组件在 React 眼里就是"半残"的。
  2. 表单元素绑定的属性各不相同:文本框/文本域用 value,复选框用 checked,取值时对应 e.target.value 与 e.target.checked,别混用。
  3. 能用状态操作表单值就不要用 ref 拿 DOM 值——受控组件符合数据驱动视图的思想,ref 拿值只是"能跑"而非"该跑"。
  4. useRef 的正确使用节奏是:渲染阶段只创建和绑定,DOM 操作一律推迟到挂载完成之后(事件回调 / useEffect)。
  5. props 是父传子的单向只读通道,React 用函数参数接收,用解构 + 默认值写法最简洁;非字符串必须加花括号。
  6. 父子通讯记住一句话:谁的数据谁负责——父组件出数据也出修改函数,子组件只负责"调用 + 回传",数据流向永远是单向闭环。
  7. 兄弟组件通讯靠"状态提升":把共享数据提到公共父级,读走父到子,改走子到父,本质仍是父子通讯的组合。
  8. 层级过深时用 Context 消除 props 逐层传递,Provider 划定范围并给 value,范围内任意层级(包括子级、后代级)用 useContext 取用;数据可以共享,但修改逻辑仍要留在提供数据的那一层。