React 通过以事件名的驼峰形式命名的 props 来处理事件,比如 onClick、onChange 和 onSubmit。你传入一个函数,当用户点击、输入或提交时,React 会带着一个事件对象调用它。
每次点击都会执行 handleClick,它更新计数,并在预览下方的控制台打印日志。把 onClick 换成 onDoubleClick 试试:现在只有双击才会计数。
传递函数,而不是调用它
这个 prop 接收一个函数,由 React 稍后调用。一个非常常见的错误是加上括号,这样函数会在渲染时立刻被调用,交给 React 的是它的返回值。
你还什么都没动,控制台就显示了“Hi, during render”:React 在渲染第一个按钮时执行了 sayHi,而 sayHi 返回 undefined,所以那个按钮根本没有处理函数。点击 Wrong 什么都不会发生。点击 Right 会打印“Hi, Ada”,因为箭头函数只在点击时运行。
箭头函数也是传参的方式。onClick={() => removeItem(item.id)} 为每一项创建一个小函数,这个函数再用正确的 ID 调用你的函数。如果处理函数不需要参数,直接按名字传递即可:onClick={handleClick}。
如果是状态的 setter,这个错误会更严重:onClick={setCount(count + 1)} 会在渲染期间设置状态,于是重新渲染,又设置状态,最后 React 报错“Too many re-renders”并停下。
事件对象
React 会给每个处理函数传入一个参数,即事件对象。它和浏览器的事件拥有相同的属性:e.target 是事件发生的元素,e.key 是按下的键,e.preventDefault() 取消浏览器的默认行为。React 把原生事件包装成所谓的合成事件,让它在所有浏览器中表现一致;如果真的需要,原始事件在 e.nativeEvent 里。
最常用的两个是 onChange 中的 e.target.value 和 onKeyDown 中的 e.key。
输入 hooks 并按 Enter:标签被添加,输入框被清空。控制台会用 e.key 中的名字记录每一次按键,包括 Shift、Backspace 和 Enter。输入到一半时按 Escape,可以丢弃这个词。
React 的 onChange 在每次按键时都会触发。在普通 HTML 中,文本框的 change 事件只在失去焦点时触发,所以这是一个有意为之的区别:它让输入框和状态逐字保持同步。复选框提供的是 e.target.checked 而不是 value。表单一页讲解了每种输入类型。
表单与 preventDefault
表单提交时,浏览器的默认行为是发送数据并加载一个新页面。在 React 应用中,你几乎总是想自己处理这些数据,所以提交处理函数以 e.preventDefault() 开头。
把 onSubmit 放在 <form> 上,而不是把 onClick 放在按钮上:这样在字段中按 Enter 也会提交,并且浏览器自己的检查(这里是 required 和 type="email")会先运行。删掉 e.preventDefault() 这一行再提交一次:真实页面会跳转到新页面,感谢信息也就没了。预览会阻止跳转,改为显示表单将要发送的内容 email=,后面没有值,因为读取表单时 setEmail('') 已经清空了字段。
React 19 还允许你给 <form action={fn}> 传入一个函数,它会接收表单数据,并替你重置非受控字段。表单一页展示了这两种写法。
事件冒泡与 stopPropagation
大多数事件会冒泡:在你点击的元素上运行之后,它们会在父元素上运行,再到父元素的父元素,一直到顶层。React 的处理函数也遵循同样的顺序。这就是为什么点击一张可点击卡片里的按钮,也算点击了这张卡片。
点击 Bubbles up,控制台依次记录按钮、内层面板、外层卡片。点击 Stops here,只有按钮被记录:e.stopPropagation() 终止了向上传播。点击内层面板自己的文字,会记录面板和卡片。
谨慎使用 stopPropagation。一个常见的合理场景是可点击的列表行里有个删除按钮,点击删除时不应该同时打开这一行。如果父元素需要知道点击是否来自某个子元素,应该检查 e.target,而不是阻止事件。想在事件向下传递时、子元素收到之前就运行处理函数,React 提供了 onClickCapture 这样的捕获版本。
preventDefault 和 stopPropagation 是两种不同的工具。前者取消浏览器本来要做的事(跟随链接、提交表单、勾选复选框)。后者阻止其他处理函数收到这个事件。调用其中一个并不会产生另一个的效果。
其他常用事件
同样的模式适用于 React 支持的所有事件。以下几个你会经常用到:
onFocus和onBlur在输入框获得和失去焦点时触发,适合在用户离开字段后显示校验结果。onMouseEnter和onMouseLeave在指针移入和移出元素时触发。与大多数事件不同,它们不冒泡,所以指针从父元素移到子元素时,不会再次触发父元素的onMouseEnter。onPointerDown、onPointerMove和onPointerUp对鼠标、触摸和触控笔都适用,因此是实现拖拽的更好选择。onScroll在元素滚动时触发。在 React 中它不冒泡。
每个处理函数收到的事件对象都带有对它有意义的属性:指针事件有 e.clientX,键盘事件有 e.key,所有事件都有 e.target。
把处理函数作为 props 传递
组件可以像接收其他 prop 一样,从父组件接收处理函数。按发生的事情把它们命名为 onSomething,处理它的函数命名为 handleSomething。这和内置事件的命名方式一致,一眼就能看出哪些 props 是回调。
Stepper 并不拥有这个数字;它只报告点击,由父组件决定点击的含义。把 handleDecrement 改成停在 1 而不是 0,无需修改 stepper,它就会跟着变化。把状态留在父组件、再把处理函数向下传递,正是状态提升所讲的模式。
自定义的处理函数 props 只是名字而已。你自己组件上的 onIncrement 什么都不做,直到组件把它传给真实元素的 onClick;你也可以给它传入任何参数,比如 onSelect(item.id)。
常见问题
如何给 onClick 处理函数传参?
用箭头函数把调用包起来:onClick={() => deleteItem(item.id)}。点击时 React 调用的是这个箭头函数,它再带着参数调用你的函数。
为什么我的 onClick 在页面加载时就执行了?
你调用了函数,而不是传递它。onClick={handleClick()} 会在渲染时执行 handleClick,并把它的返回值传进去。应该写 onClick={handleClick} 或 onClick={() => handleClick(id)}。
React 的 onChange 和 HTML 的有什么区别?
在 HTML 中,文本输入框的 change 事件在失去焦点时才触发。React 的 onChange 则在每次按键时触发,和原生的 input 事件一样,所以状态能跟着输入的文字实时变化。
在 React 中如何阻止表单刷新页面?
在 onSubmit 处理函数的开头调用 e.preventDefault()。把处理函数放在 <form> 上,而不是提交按钮上,这样在字段里按 Enter 也能提交。
React 中的合成事件是什么?
它是 React 传给处理函数的事件对象。它包装了浏览器的原生事件,拥有相同的属性和方法(target、key、preventDefault()、stopPropagation()),并且在各个浏览器中表现一致。原始事件可以通过 e.nativeEvent 获取。