Next.js循环渲染MUI TextField 提交取值与状态问题修复


问题说明
基于Next.js + MUI开发的写作答题页存在两处功能异常:
- 全局维护单一字数统计状态,切换题目后字数显示沿用上一题的值,无法做到每道题独立统计
- 题目切换时非当前题的输入框被卸载,已输入内容丢失,且提交时无法聚合所有题目的答案
修复方案
核心改动是将单值的答案、字数状态改为以题目ID为键的对象结构存储,输入框改为受控组件绑定对应状态,既可以保留切换时的输入内容,也能实现单题字数独立统计,提交时直接遍历状态对象即可拿到所有答案。
同时优化了原来硬编码的按钮显隐逻辑,改为根据当前题目索引自动判断按钮展示状态,适配任意数量的题目场景。
修复后完整代码
import { Grid, Typography, Box, NoSsr, TextField } from '@mui/material'; import PersonIcon from '@mui/icons-material/Person'; import Timer from '../../../components/timer'; import Button from '@mui/material/Button'; import { useState } from 'react'; import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; import { Scrollbars } from 'react-custom-scrollbars'; import AppBar from '@mui/material/AppBar'; import Toolbar from '@mui/material/Toolbar'; import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos'; import axios from '../../../lib/axios'; import { decode } from 'html-entities'; import { blueGrey } from '@mui/material/colors'; export default function Writing({ questions }) { const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); // 存储所有题目答案,key为题目id,value为输入内容 const [answers, setAnswers] = useState({}); // 存储所有题目字数统计,key为题目id,value为对应字数 const [wordCounts, setWordCounts] = useState({}); // 输入变化处理 const handleAnswerChange = (questionId, value) => { // 更新对应题目的答案 setAnswers(prev => ({ ...prev, [questionId]: value })); // 计算并更新对应题目的字数,空内容时字数为0 const count = value.trim() ? value.trim().split(/\s+/).length : 0; setWordCounts(prev => ({ ...prev, [questionId]: count })); }; // 提交处理 const handleSubmit = () => { // 此处可直接拿到所有题目的答案做接口提交 console.log('所有作答内容:', answers); // 示例提交逻辑 // axios.post('提交地址', { answers }) }; return ( <> <Box sx={{ flexGrow: 1 }}> <AppBar position="fixed" style={{ background: blueGrey[900] }}> <Toolbar> <Grid container spacing={2} alignItems="center"> <Grid item xs={4} display="flex" alignItems="center"> <PersonIcon sx={{ background: '#f2f2f2', borderRadius: '50px' }} /> <Typography variant="h6" color="#f2f2f2" ml={1}> xxxxx xxxxx-1234 </Typography> </Grid> <Grid item xs={4} container justifyContent="center"> <Timer timeValue={2400} /> </Grid> <Grid item xs={4} container justifyContent={'right'}> <Button variant="contained" style={{ background: 'white', color: 'black' }} size="small"> Settings </Button> <Button variant="contained" style={{ background: 'white', color: 'black', margin: '0px 10px', }} size="small"> Hide </Button> <Button variant="contained" style={{ background: 'white', color: 'black' }} size="small"> Help </Button> </Grid> </Grid> </Toolbar> </AppBar> </Box> <Box sx={{ background: blueGrey[50], height: '100%', width: '100%', position: 'absolute', }} pt={{ xs: 13, sm: 11, md: 10, lg: 11, xl: 11 }}> {questions.map((question, index) => index === currentQuestionIndex ? ( <Box key={question.id} px={3} sx={{ background: '#f2f2f2', pb: 4 }} position={{ xs: 'sticky', sm: 'sticky', lg: 'initial', md: 'initial', xl: 'initial', }}> <Box style={{ background: '#f7fcff', borderRadius: '4px' }} py={1} px={2}> <Box> <Typography variant="h6" component="h6" ml={1}> Part {question.id} </Typography> <Typography variant="subtitle2" component="div" ml={1} mt={1}> <NoSsr> <div dangerouslySetInnerHTML={{ __html: decode(question.questions[0].question, { level: 'html5', }), }}></div> </NoSsr> </Typography> </Box> </Box> <Box style={{ background: '#f7fcff', borderRadius: '4px', marginBottom: '75px', }} pt={1} px={3} mt={{ xs: 2, sm: 2, md: 2, lg: 0, xl: 3 }}> <Grid container spacing={2}> <Grid item xs={12} sm={12} lg={6} md={6} xl={6}> <Box py={{ lg: 1, md: 1, xl: 1 }} style={{ height: '50vh' }}> <Scrollbars universal> <Typography variant="body1" component="div" style={{ textAlign: 'justify' }} mr={2}> <NoSsr> <div dangerouslySetInnerHTML={{ __html: decode(question.question_text, { level: 'html5', }), }}></div> </NoSsr> </Typography> </Scrollbars> </Box> </Grid> <Grid item xs={12} sm={12} lg={6} md={6} xl={6} mt={{ md: 4, lg: 4, xl: 4 }}> <TextField id={`${question.id}`} label="Type your answer here" multiline name={`answer_${question.id}`} rows={12} variant="outlined" fullWidth // 绑定受控值,从状态中取当前题目的答案 value={answers[question.id] || ''} helperText={`Words Count: ${wordCounts[question.id] || 0}`} onChange={(e) => { handleAnswerChange(question.id, e.target.value); }} /> </Grid> </Grid> </Box> </Box> ) : null )} <Box sx={{ position: 'fixed', width: '100%', left: 0, bottom: 0 }}> <Grid container style={{ background: blueGrey[300], display: 'flex' }} py={2} px={3}> <Grid item xs={3} sm={3} lg={6} md={6} xl={6} container justifyContent={'start'}> <Button variant="contained" style={{ background: 'white', color: 'black' }} size="small"> Save Draft </Button> </Grid> <Grid item xs={9} sm={9} lg={6} md={6} xl={6} container justifyContent={'end'} gap={1} > {/* 上一题按钮:不是第一题时显示 */} {currentQuestionIndex > 0 && ( <Button variant="contained" style={{ background: 'white', color: 'black', }} startIcon={<ArrowBackIosIcon />} size="small" onClick={() => { setCurrentQuestionIndex(prev => prev - 1); }}> Previous Part </Button> )} {/* 下一题按钮:不是最后一题时显示 */} {currentQuestionIndex < questions.length - 1 && ( <Button variant="contained" size="small" style={{ background: 'white', color: 'black', }} endIcon={<ArrowForwardIosIcon />} onClick={() => { setCurrentQuestionIndex(prev => prev + 1); }}> Next Part </Button> )} {/* 提交按钮:最后一题时显示 */} {currentQuestionIndex === questions.length - 1 && ( <Button variant="contained" color="success" onClick={handleSubmit}> Submit Test </Button> )} </Grid> </Grid> </Box> </Box> </> ); } export async function getServerSideProps(context) { const { id } = context.query; const token = context.req.cookies.token; if (!token) { context.res.writeHead(302, { Location: '/', }); context.res.end(); } const res = await axios.get(`test/${id}/questions`, { headers: { Authorization: `Bearer ${token}`, }, }); if (res.data.success) { return { props: { questions: res.data.data.questions, }, }; } }
改动点说明
- 删除了原来冗余的
show、show1布尔显隐状态,统一通过currentQuestionIndex判断当前题目和按钮展示逻辑,适配任意题目数量 - 新增
answers对象存储所有题目输入内容,输入框改为受控组件,切换题目时内容持久化保存在状态中不会丢失 - 新增
wordCounts对象按题目ID单独存储字数,每个题目的字数统计独立互不干扰 - 新增
handleSubmit提交方法,可直接获取所有题目的答案用于接口请求 - 修复了空输入时字数统计为1的边界问题,空内容时字数显示为0
内容的提问来源于stack exchange,提问作者DCodeMania
相关产品推荐
相关产品推荐

