如何测试使用useFormState的Next.js组件?含替代方案
问题
开发Next.js项目时,组件使用了react-dom的useFormState钩子,用Jest编写单元测试时出现错误:
TypeError: (0 , _reactdom.useFormState) is not a function or its return value is not iterable
组件简化代码:
// Inside a Next.js project import { useState } from 'react' import { useFormState } from 'react-dom' export default function MyFormComponent() { const [state, formAction] = useFormState(/* ... */); // ... rest of the component }
测试代码:
import '@testing-library/jest-dom' import { render, screen } from '@testing-library/react' import MyCustomComponent from '../page' describe('MyCustomComponent', () => { it('renders a heading', () => { render(<MyCustomComponent/>); screen.debug(); }) })
解决方案
一、有效测试使用useFormState的Next.js组件
1. 正确模拟useFormState钩子
useFormState是React 18.2+和Next.js 13+的特性,Jest测试时需要手动模拟该钩子,确保返回可迭代的数组(状态+处理函数)。在测试文件顶部添加以下模拟代码:
jest.mock('react-dom', () => ({ ...jest.requireActual('react-dom'), useFormState: jest.fn(() => [/* 自定义初始状态,比如{}或'' */, jest.fn()]) }));
该模拟会保留react-dom的其他原生导出,仅替换useFormState,避免破坏其他功能。
2. 细化模拟适配业务逻辑
如果组件中useFormState绑定了特定的表单处理函数,可以让模拟逻辑更贴近真实行为,比如让返回的formAction返回Promise:
jest.mock('react-dom', () => ({ ...jest.requireActual('react-dom'), useFormState: jest.fn((action, initialState) => [ initialState, jest.fn(() => Promise.resolve(initialState)) ]) }));
这样可以支持测试异步提交场景,更符合真实业务流程。
3. 确保测试环境配置正确
Next.js默认的Jest配置已适配React/React DOM环境,若自定义了jest.config.js,需注意:
- 不要手动修改React版本依赖
- 确保
babel-jest或swc-jest正确处理Next.js模块转换
二、易测试的Next.js表单状态管理替代方案与最佳实践
1. React原生useState+手动提交逻辑
对于简单表单,直接用useState管理输入值,手动处理提交,无需额外依赖,测试成本最低:
export default function MyFormComponent() { const [email, setEmail] = useState(''); const handleSubmit = async (e) => { e.preventDefault(); // 执行提交逻辑,比如调用Server Action }; return ( <form onSubmit={handleSubmit}> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} /> <button type="submit">提交</button> </form> ); }
测试时可直接模拟输入、触发提交,断言状态变化或提交行为。
2. 使用react-hook-form
社区主流表单库,体积轻量、性能优异,且测试友好。提供专门的测试工具,无需复杂钩子模拟:
import { useForm } from 'react-hook-form'; export default function MyFormComponent() { const { register, handleSubmit } = useForm(); const onSubmit = (data) => { // 处理表单数据,比如调用接口 }; return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register('email', { required: true })} /> <button type="submit">提交</button> </form> ); }
测试时用@testing-library/react模拟输入、触发提交,直接断言表单数据是否正确收集。
3. Server Actions结合useState
若需要后端处理逻辑,可分离前端状态与Server Action:用useState管理前端输入,提交时直接调用Server Action,避免依赖useFormState,测试时只需模拟Server Action即可。
内容的提问来源于stack exchange,提问作者Javier

