跳到主要内容

8 篇博文 含有标签「react」

查看所有标签

HTML表单提交的两种方式:为什么有些按钮不触发 onSubmit

· 阅读需 4 分钟
Claude
Anthropic Claude
1adybug
子虚伊人

以下内容均为 Claude 4 sonnet 生成

在前端开发中,我们经常会遇到表单提交的问题。今天就来深入探讨一个看似简单却容易让人困惑的问题:为什么有些按钮能触发表单的 onSubmit 事件,而有些却不能?

问题场景

让我们先看一个具体的例子:

import { FC, FormEvent, useRef } from "react"

const App: FC = () => {
const form = useRef<HTMLFormElement>(null)

function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
console.log("表单已提交!")
}

return (
<div>
<form ref={form} onSubmit={onSubmit}>
<input name="name" type="text" />
<button type="submit">内部提交按钮</button>
</form>
<button onClick={() => form.current?.submit()}>外部提交按钮</button>
</div>
)
}

运行这段代码你会发现:

  • 点击"内部提交按钮":控制台输出"表单已提交!"
  • 点击"外部提交按钮":控制台没有任何输出

这是为什么呢?

原理解析

方式一:标准表单提交(触发 onSubmit)

<button type="submit">内部提交按钮</button>

当我们点击这个按钮时,发生了以下过程:

  1. 浏览器识别到这是一个 type="submit" 的按钮
  2. 浏览器查找该按钮所属的表单
  3. 浏览器触发表单的 submit 事件
  4. 我们的 onSubmit 事件处理函数被调用
  5. 如果没有 preventDefault(),表单会被实际提交

这是标准的HTML表单提交流程,完全符合Web标准。

方式二:程序化提交(不触发 onSubmit)

<button onClick={() => form.current?.submit()}>外部提交按钮</button>

当我们点击这个按钮时:

  1. 执行 onClick 处理函数
  2. 调用 form.submit() 方法
  3. 表单被直接提交,但不触发 submit 事件
  4. onSubmit 处理函数不会被调用

关键点:根据HTML规范,程序化调用 HTMLFormElement.submit() 方法会绕过表单验证和事件触发机制。

为什么会有这种设计?

这种设计是有意为之的:

  1. 性能考虑:程序化提交通常用于自动化场景,跳过事件处理可以提高性能
  2. 避免无限循环:如果在 onSubmit 中调用 form.submit(),可能导致无限递归
  3. 明确区分:用户操作和程序操作应该有不同的行为模式

实际应用中的解决方案

方案一:手动调用事件处理函数

<button onClick={(e) => {
if (form.current) {
// 手动调用 onSubmit 处理函数
const syntheticEvent = {
...e,
currentTarget: form.current,
target: form.current,
preventDefault: () => {}
}
onSubmit(syntheticEvent as FormEvent<HTMLFormElement>)
}
}}>外部提交按钮</button>

方案二:使用 requestSubmit()(推荐)

<button onClick={() => form.current?.requestSubmit()}>外部提交按钮</button>

requestSubmit() 是HTML5的新方法,它:

  • 会触发 submit 事件
  • 会执行表单验证
  • 行为类似于点击提交按钮

注意requestSubmit() 的浏览器兼容性比 submit() 稍差,在一些旧版本浏览器中不支持。

方案三:模拟点击提交按钮

const submitButtonRef = useRef<HTMLButtonElement>(null)

// 在JSX中
<form ref={form} onSubmit={onSubmit}>
<input name="name" type="text" />
<button ref={submitButtonRef} type="submit">内部提交按钮</button>
</form>
<button onClick={() => submitButtonRef.current?.click()}>外部提交按钮</button>

这种方式通过模拟点击来触发标准的表单提交流程。

最佳实践建议

  1. 优先使用标准提交按钮:在表单内使用 type="submit" 的按钮
  2. 需要程序化提交时优先考虑 requestSubmit():它更符合标准行为
  3. 做好兼容性处理:如果需要支持旧版本浏览器,提供降级方案
  4. 保持一致的用户体验:确保所有提交方式都有相同的验证和处理逻辑

总结

表单提交看似简单,实则涉及Web标准、浏览器实现和用户体验的多个层面。理解 submit()requestSubmit() 的区别,以及它们与事件处理机制的关系,能帮助我们写出更健壮的表单处理代码。

记住这个核心原则:用户触发的提交会触发事件,程序触发的提交通常不会。掌握了这一点,你就能更好地控制表单的提交行为了。

记录一次由 React Compiler 引起的 "bug"

· 阅读需 3 分钟
Claude
Anthropic Claude

以下内容均为 Claude 4 sonnet 生成

问题背景

最近在项目中启用了 React Compiler 后,遇到了一个令人困惑的问题。一段看似正常的代码在没有启用 React Compiler 时运行良好,但启用后却会在渲染阶段直接崩溃。

问题代码

import { FC, useState } from "react"

interface Info {
name: string
age: number
}

const App: FC = () => {
const [info, setInfo] = useState<Info | undefined>(undefined)

function onClick() {
console.log(info!.name) // 使用了非空断言
}

return <div onClick={onClick} />
}

export default App

问题现象

启用 React Compiler 后,即使 onClick 事件从未被触发,应用也会在渲染过程中直接崩溃,报错:

Cannot read property 'name' of undefined

问题分析

React Compiler 的工作原理

React Compiler 是一个编译时优化工具,它会:

  1. 依赖分析:分析函数组件中的依赖关系
  2. 自动记忆化:为函数和值自动添加 useMemouseCallback
  3. 代码优化:重新组织代码以提高性能

问题根源

React Compiler 在分析 onClick 函数时,发现它引用了 info.name,因此将 info 作为依赖项。为了优化性能,编译器可能会:

  1. onClick 函数包装在 useCallback
  2. info.name 的访问提前到渲染阶段进行依赖收集
  3. 这导致在 info 还是 undefined 的初始渲染时就尝试访问 info.name

编译后的大致效果

// 编译器可能生成类似这样的代码
const App: FC = () => {
const [info, setInfo] = useState<Info | undefined>(undefined)

// 编译器为了依赖收集,可能在渲染时就访问了 info.name
const onClick = useCallback(() => {
console.log(info!.name)
}, [info?.name]) // 注意这里的依赖

return <div onClick={onClick} />
}

解决方案

方案一:使用可选链操作符

function onClick() {
console.log(info?.name) // 使用可选链
}

方案二:添加条件判断

function onClick() {
if (info) console.log(info.name)
}

方案三:使用默认值

const [info, setInfo] = useState<Info>({ name: "", age: 0 })

经验总结

1. 避免在可能为空的对象上使用非空断言

非空断言 (!) 只是告诉 TypeScript 编译器忽略空值检查,但运行时仍可能出错。

2. React Compiler 改变了代码执行时机

启用 React Compiler 后,一些原本在事件处理中才会执行的代码可能会在渲染时执行。

3. 防御性编程的重要性

始终考虑变量可能为空的情况,使用可选链和条件判断。

4. 理解工具的工作原理

了解 React Compiler 等工具的工作机制,有助于预防和解决类似问题。

最佳实践建议

  1. 启用严格的 TypeScript 配置:使用 strict: truestrictNullChecks: true

  2. 优先使用可选链:在访问可能为空的对象属性时使用 ?.

  3. 避免过度使用非空断言:只在确实知道值不为空时使用 !

  4. 渐进式启用新工具:在小范围内测试新的编译工具,逐步推广

  5. 完善的错误边界:设置 Error Boundary 来捕获和处理渲染时错误

结语

这个案例提醒我们,新的编译优化工具虽然能带来性能提升,但也可能改变代码的执行行为。作为开发者,我们需要:

  • 理解工具的工作原理
  • 编写更加健壮的代码
  • 进行充分的测试
  • 保持对新技术的学习和适应

希望这个案例能帮助其他开发者避免类似的问题。

重新学习 React Router

· 阅读需 2 分钟
1adybug
子虚伊人

前言

React Router 在推出 Remix 后变成了一个全栈框架,在 V7 版本之后,更是将 React RouterRemix 合并在一起,提供了一个全栈的解决方案。

常见问题

  1. loaderclientLoader 有什么区别?

    • 执行环境

      • loader: 在服务器端执行
      • clientLoader: 在客户端(浏览器)执行
    • 运行时机

      • loader: 在页面初始加载和每次导航时在服务器上运行
      • clientLoader: 在客户端导航时运行,首次页面加载时不执行
    • 数据获取能力

      • loader: 可以访问服务器资源(数据库、文件系统、内部API等)
      • clientLoader: 只能访问浏览器可用的资源(如公共API、localStorage等)
    • 安全性

      • loader: 可以包含敏感逻辑和凭证,因为代码不会发送到客户端
      • clientLoader: 所有代码会发送到浏览器,不应包含敏感信息
    • 用途场景

      • loader: 适用于需要服务器权限或敏感数据的操作
      • clientLoader: 适用于提升客户端导航体验,减轻服务器负担的场景

    这两个函数通常可以配合使用:loader 用于初始数据加载和服务器相关操作,clientLoader 用于优化后续的客户端导航体验。

    loader 将在你首次打开页面就是这个路由时执行,clientLoader 将在你由其他路由导航到这个路由时执行。clientLoader 在首次页面加载时不会执行。在 clientLoader 中,你可以使用 serverLoader 调用 loader

    import type { Route } from "./+types/product"

    // route("products/:pid", "./product.tsx");
    import { fakeDb } from "../db"

    export async function loader({ params }: Route.LoaderArgs) {
    return fakeDb.getProduct(params.pid)
    }

    export async function clientLoader({ serverLoader, params }: Route.ClientLoaderArgs) {
    const res = await fetch(`/api/products/${params.pid}`)
    const serverData = await serverLoader()
    return { ...serverData, ...res.json() }
    }

    export default function Product({ loaderData }: Route.ComponentProps) {
    const { name, description } = loaderData

    return (
    <div>
    <h1>{name}</h1>
    <p>{description}</p>
    </div>
    )
    }

React Strict Mode

· 阅读需 3 分钟
1adybug
子虚伊人

以下是 React 官网的解释:

`React` 中,`Strict Mode` 是一个用于开发环境的工具,用于帮助开发者发现潜在的问题。它会在开发环境中执行额外的检查,并提供警告信息。

严格模式启用了以下仅在开发环境下有效的行为:

- 组件将 额外重新渲染一次 以查找由于非纯渲染而引起的错误。
- 组件将 额外重新运行一次 `Effect` 以查找由于缺少 `Effect` 清理而引起的错误。
- 组件将 额外重新运行一次 `refs` 回调 以查找由于缺少 `ref` 清理函数而引起的错误。
- 组件将被 检查是否使用了已弃用的 `API`

以下是测试代码:

import { FC, useEffect, useMemo, useRef, useState } from "react"

let stateCache: any
let refCache: any
let memoCache: any
let stateCache2: any
let refCache2: any
let memoCache2: any

const App: FC = () => {
const [state, setState] = useState(() => (console.log("calculate state"), {}))
const ref = useRef({})
const memo = useMemo(() => (console.log("calculate memo"), {}), [])

if (stateCache) console.log(state === stateCache)
else stateCache = state

if (refCache) console.log(ref === refCache, ref.current === refCache.current)
else refCache = ref

if (memoCache) console.log(memo === memoCache)
else memoCache = memo

useEffect(() => {
if (stateCache2) console.log(state === stateCache2, stateCache === stateCache2)
else stateCache2 = state

if (refCache2) console.log(ref === refCache2, ref.current === refCache2.current)
else refCache2 = ref

if (memoCache2) console.log(memo === memoCache2, memoCache === memoCache2)
else memoCache2 = memo
}, [])

return <div></div>
}

export default App

在 React 18 中,以下是打印结果:

calculate state
calculate memo
calculate state
calculate memo
false
false false
false
true false
true true false false
true false

可以看出,组件额外渲染了一次,staterefmemo 都额外重新获取了一次值,并且返回的第二次获取的新值。

Effect 额外运行了一次,但是获取到的 staterefmemo 的值是相同的,并且是第二次获取的新值

在 React 19 中,以下是打印结果:

calculate state
calculate state
calculate memo
calculate memo
true
true true
true
true true
true true true true
true true

可以看出,组件额外渲染了一次,staterefmemo 都额外重新获取了一次值,然而返回的却是第一次获取的旧值。

Effect 额外运行了一次,获取到的 staterefmemo 的值是依然是相同的,并且是第一次获取的旧值,

以下是 React 官网的解释:

### StrictMode changes

React 19 includes several fixes and improvements to Strict Mode.

When double rendering in Strict Mode in development, useMemo and useCallback will reuse the memoized results from the first render during the second render. Components that are already Strict Mode compatible should not notice a difference in behavior.

As with all Strict Mode behaviors, these features are designed to proactively surface bugs in your components during development so you can fix them before they are shipped to production. For example, during development, Strict Mode will double-invoke ref callback functions on initial mount, to simulate what happens when a mounted component is replaced by a Suspense fallback.
区别React 18React 19
组件额外渲染一次
staterefmemo 额外重新获取一次值
render 阶段获取到的 staterefmemo旧值/新值旧值/旧值
Effect 在首次渲染时额外运行一次并且立即执行卸载
Effect 阶段获取到的 staterefmemo新值/新值旧值/旧值

在 react 中手动触发 change 事件

· 阅读需 2 分钟
1adybug
子虚伊人

react 中存在很多受控组件,比如 inputtextarea 等,一般来说,受控组件将会是如下写法:

import { FC } from "react"

const App: FC = () => {
const [value, setValue] = useState("")

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

export default App

这种写法在 react 内部使用没有任何问题,但是对于外部使用者来说却存在很多问题,比如:

import { FC, useState } from "react"

const App: FC = () => {
const [value, setValue] = useState("")

return (
<div>
<div>
<label htmlFor="input">input:</label>
<input id="input" value={value} onChange={e => setValue(e.target.value)} />
</div>
<div>value:{value}</div>
</div>
)
}

export default App

当我们使用如下代码来更改 input 的值:

input.value = "123456"

却看到输入框的值发生了更改,下方的文本却没有任何变化,说明我们更改 input 的值的行为并没有触发 react 内部的 change 事件,那我们来手动触发 changeinput 事件:

const changeEvent = new Event("change", { bubbles: true })
const inputEvent = new Event("input", { bubbles: true })

input.dispatchEvent(changeEvent)
input.dispatchEvent(inputEvent)

可以看到,下方文本任然没有任何变化,Google 一番终于找到了正确答案 Simulate React On-Change On Controlled Components

const set = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set

set?.call(input, "123456")

const inputEvent = new Event("input", { bubbles: true })

input.dispatchEvent(inputEvent)

下方的文本成功同步!

React Native 和 Expo 开发中的问题汇总

· 阅读需 1 分钟
1adybug
子虚伊人

无法连接到开发服务器

注意

could not connect to the development server

解决方案:

  1. 在虚拟机中连接 WiFi
  2. 在 Android Studio 中将 GradleJava 位置设置为系统位置

项目编译失败

解决方案:

在 Android Studio 中打开 android 目录

动画暂停之后速度变慢

原因:

动画暂停之后再开始,会以设置的全部时间来完成剩余的进度

解决方案:

/** 初始值 */
const fromValue = 0

/** 重点值 */
const toValue = 100

/** 时间 */
const duration = 10000

/** 速度 */
const speed = (toValue - fromValue) / duration

/** 动画值 */
const translateX = useRef(new Animated.Value(fromValue)).current

/** 暂停值 */
const stopValue = useRef(fromValue)

/** 动画状态 */
const status = useRef(false)

function onClick() {
// 如果暂停值已经达到目标值,说明动画已经完成
if (stopValue.current === toValue) return

// 如果动画处于播放状态,暂停动画,并且将当前值赋予给暂停值
if (status.current) translateX.stopAnimation(value => (stopValue.current = value))
// 否则播放动画
else {
Animated.timing(translateX, {
toValue,
duration: (toValue - stopValue.current) / speed,
useNativeDriver: true,
easing: Easing.linear,
}).start(({ finished }) => finished && (stopValue.current = toValue))
}

status.current = !status.current
}

HTML 元素 props

· 阅读需 1 分钟
1adybug
子虚伊人

很多时候我们会有元素标签上显示的属性来获取元素的 props

import { HTMLAttributes } from "react"

type MyDivProps = HTMLAttributes<HTMLDivElement>

但其实,React 内置更为方便的泛型工具 ComponentProps

import { ComponentProps } from "react"

type MyDivProps = ComponentProps<"div">

HTMLAttributes<HTMLDivElement>ComponentProps<"div"> 的区别在于,后者包含了 refkey 属性

检查依赖中的包

· 阅读需 2 分钟
1adybug
子虚伊人

有时候,项目中可能会存在某个隐藏的依赖,又或者存在同一个依赖的多个版本,这时候我们可以通过以下命令来检查依赖来源:

# 推荐:npm
npm ls @types/react
# yarn
yarn why @types/react

当项目中存在多个版本的 @types/react 时,可能或报错:

不能用作 JSX 组件, 不是有效的 JSX 元素

这时我们可以在 package.json 中配置 resolutions 统一 @types/react 版本:

{
"resolutions": {
"@types/react": "^18.2.79"
}
}

以下是实际遇到的案例,在启动某项目时,总是报以下错误:

错误

去 github 一搜,发现是项目中存在多个版本 string-width 依赖(实际上只会安装最新的依赖)的问题,使用 npm ls string-width 查看:

之前

将依赖项 string-width 版本改为 ^4

{
"resolutions": {
"string-width": "^4"
}
}

再使用 npm ls string-width 查看:

之前

问题解决了。

注意
  1. 尽量使用多个版本中最低 major 版本的依赖,新版本可能是移除了某些特性
  2. 解决这一个依赖的版本问题后,再次启动项目,可能还会报错,不要慌张,仔细看一下报错信息,有可能是另一个依赖的版本问题