React父组件点击添加按钮前,如何获取子组件数据并校验输入?
解决方案
要实现点击「Add Question」时先校验所有已有表单再添加新组件,有两种优雅的实现思路,可根据组件设计需求选择:
方案一:通过 Refs 调用子组件的校验方法
这种方式适合让子组件独立维护自身表单状态,父组件仅负责触发校验和添加逻辑。
1. 改造父组件 CreateGame
使用 useRef 存储所有 QuestionForm 的实例引用,点击按钮时遍历调用子组件的校验方法:
import { useState, useRef } from 'react'; import { nanoid } from 'nanoid'; function CreateGame() { const [questionsArray, setQuestionsArray] = useState([ { value: true, id: nanoid() }, ]); // 创建ref数组存储子组件实例 const questionFormRefs = useRef([]); const addQuestion = () => { // 遍历所有子组件,执行校验 let allValid = true; questionFormRefs.current.forEach(ref => { if (ref && !ref.validate()) { allValid = false; } }); // 所有表单校验通过才添加新组件 if (allValid) { setQuestionsArray(prevState => [ ...prevState, { value: true, id: nanoid() }, ]); } }; return ( <div id="create-game"> <h1>Quiz Creator</h1> <div className="container"> {questionsArray.map((question, index) => ( <QuestionForm key={question.id} id={question.id} // 将ref存入数组,注意index对应 ref={el => questionFormRefs.current[index] = el} /> ))} </div> <div className="buttons"> <button onClick={addQuestion}> Add Question </button> <button>Create Quiz</button> </div> </div> ); }
2. 改造子组件 QuestionForm
使用 forwardRef 暴露校验方法,实现自身字段的校验逻辑:
import { useState, forwardRef, useImperativeHandle } from 'react'; const QuestionForm = forwardRef((props, ref) => { const [formData, setFormData] = useState({ question: '', answerOne: '', answerTwo: '', answerThree: '', answerFour: '', correctAnswer: '' }); const [errors, setErrors] = useState({}); // 定义校验方法,暴露给父组件 const validate = () => { const newErrors = {}; // 检查所有字段是否为空 Object.keys(formData).forEach(key => { if (!formData[key].trim()) { newErrors[key] = '此字段不能为空'; } }); setErrors(newErrors); // 返回校验结果:无错误则通过 return Object.keys(newErrors).length === 0; }; // 暴露校验方法给父组件的ref useImperativeHandle(ref, () => ({ validate })); const handleChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); // 输入时清除对应字段的错误提示 if (errors[name]) { setErrors(prev => ({ ...prev, [name]: '' })); } }; return ( <div className="question-form"> <input name="question" value={formData.question} onChange={handleChange} placeholder="输入问题" /> {errors.question && <span className="error">{errors.question}</span>} <input name="answerOne" value={formData.answerOne} onChange={handleChange} placeholder="选项1" /> {errors.answerOne && <span className="error">{errors.answerOne}</span>} <input name="answerTwo" value={formData.answerTwo} onChange={handleChange} placeholder="选项2" /> {errors.answerTwo && <span className="error">{errors.answerTwo}</span>} <input name="answerThree" value={formData.answerThree} onChange={handleChange} placeholder="选项3" /> {errors.answerThree && <span className="error">{errors.answerThree}</span>} <input name="answerFour" value={formData.answerFour} onChange={handleChange} placeholder="选项4" /> {errors.answerFour && <span className="error">{errors.answerFour}</span>} <input name="correctAnswer" value={formData.correctAnswer} onChange={handleChange} placeholder="正确答案" /> {errors.correctAnswer && <span className="error">{errors.correctAnswer}</span>} </div> ); }); export default QuestionForm;
方案二:状态提升到父组件统一管理
这种方式将所有表单数据交给父组件维护,校验逻辑直接在父组件完成,更符合React的状态管理理念,也方便后续提交整个测验数据。
1. 改造父组件 CreateGame
维护所有问题的表单数据,点击按钮时直接校验所有数据:
import { useState } from 'react'; import { nanoid } from 'nanoid'; function CreateGame() { // 初始化包含一个空的表单数据对象 const [questionsData, setQuestionsData] = useState([ { id: nanoid(), question: '', answerOne: '', answerTwo: '', answerThree: '', answerFour: '', correctAnswer: '' } ]); const addQuestion = () => { // 校验所有已有表单数据 let allValid = true; questionsData.forEach(question => { // 跳过id字段,校验其余输入字段 Object.entries(question).forEach(([key, value]) => { if (key !== 'id' && !value.trim()) { allValid = false; } }); }); if (allValid) { // 添加新的空表单数据 setQuestionsData(prev => [ ...prev, { id: nanoid(), question: '', answerOne: '', answerTwo: '', answerThree: '', answerFour: '', correctAnswer: '' } ]); } }; const handleQuestionChange = (id, field, value) => { setQuestionsData(prev => prev.map(q => q.id === id ? { ...q, [field]: value } : q )); }; return ( <div id="create-game"> <h1>Quiz Creator</h1> <div className="container"> {questionsData.map((question) => ( <QuestionForm key={question.id} data={question} onChange={(field, value) => handleQuestionChange(question.id, field, value)} /> ))} </div> <div className="buttons"> <button onClick={addQuestion}> Add Question </button> <button>Create Quiz</button> </div> </div> ); }
2. 改造子组件 QuestionForm
变成受控组件,仅负责渲染和传递输入变更:
const QuestionForm = ({ data, onChange }) => { const handleChange = (e) => { const { name, value } = e.target; onChange(name, value); }; return ( <div className="question-form"> <input name="question" value={data.question} onChange={handleChange} placeholder="输入问题" /> <input name="answerOne" value={data.answerOne} onChange={handleChange} placeholder="选项1" /> <input name="answerTwo" value={data.answerTwo} onChange={handleChange} placeholder="选项2" /> <input name="answerThree" value={data.answerThree} onChange={handleChange} placeholder="选项3" /> <input name="answerFour" value={data.answerFour} onChange={handleChange} placeholder="选项4" /> <input name="correctAnswer" value={data.correctAnswer} onChange={handleChange} placeholder="正确答案" /> </div> ); }; export default QuestionForm;
方案选择建议
- 如果
QuestionForm需要复杂的独立校验逻辑(比如字段格式验证、实时错误提示),选方案一,子组件可以自主处理校验细节。 - 如果后续需要统一提交所有测验数据,或者希望集中管理状态,选方案二,父组件直接持有所有数据,后续扩展更方便。
内容的提问来源于stack exchange,提问作者Bruno Oliveira
相关产品推荐
相关产品推荐

