useFormik与useState结合时输入框值无法更新的问题排查
问题
我在开发电商后台的分类创建表单,每个分类包含ParentId(0代表一级分类,其他值代表子分类)。我做了一个电商通用目录按钮,想实现点击展开菜单选择分类后,把该分类的ParentId填入输入框(比如选“服饰鞋包”后输入框显示其ID8)。
我用useState创建了parentId状态,点击菜单选项时能正常更新这个状态,但提交表单时Formik里的parentId值始终没变化,始终是初始的0。相关代码如下:
import React, {FC, useState} from "react"; import {Box, Button, Stack, SxProps, TextField} from "@mui/material"; import {useFormik} from "formik"; // own modules import Catalog from "../catalog/Catalog"; const CreateCategory: FC = (sx?: SxProps) => { const [parentId, setParentId] = useState<number>(0); const formik = useFormik({ initialValues: { name: "", label: "", parentId: parentId }, onSubmit: (values, {setSubmitting}) => { console.log("values: ", values); // value of the parentId doesn't change on submit // ...other code... setSubmitting(false); } }); console.log("parentId: ", parentId); // but value of the parentId in the state change properly return ( <Box sx={{ display: "grid", justifyContent: "flex-start", gridTemplateColumns: "1fr 1fr 1fr 1fr", gap: "2rem", "> *": { gridColumn: "span 2" }, ...sx }} component="form" onSubmit={formik.handleSubmit} > <TextField error={!!(formik.errors.name && formik.touched.name)} name="name" onChange={formik.handleChange} value={formik.values.name} label="Name (technical - in Latin)" variant="outlined" helperText={formik.errors.name} /> <TextField error={!!(formik.errors.label && formik.touched.label)} name="label" onChange={formik.handleChange} value={formik.values.label} label="label(for users)" variant="outlined" helperText={formik.errors.label} /> <Stack direction="row" spacing={2}> <TextField sx={{flexGrow: 1}} error={!!(formik.errors.parentId && formik.touched.parentId)} name="parentId" value={parentId} label="parentId" variant="outlined" helperText={formik.errors.parentId} /> <Catalog onClickOverload={setParentId} /> {/*custom catalog button*/} <Button sx={{height: "100%", width: "max-content"}} onClick={() => setParentId(0)} // you can change this value(0 to 10, example) to check, that initial value(0) doesn't change in the formik values > Set parent (0) </Button> </Stack> <Button sx={{gridColumnStart: 2, gridColumnEnd: 4}} type="submit" disabled={formik.isSubmitting} variant="outlined"> Create </Button> </Box> ) } export default CreateCategory;
错误原因与解决方法
核心问题
你把Formik的表单状态和自己的parentId状态分开维护了:
- Formik的
initialValues只会在组件初始化时读取一次parentId的初始值0,之后外部parentId变化不会自动同步到Formik内部的values里 - 你给ParentId对应的TextField绑定的是自己的
parentId状态,但没有把这个值同步回Formik的表单状态,所以Formik完全不知道这个值发生了变化
两种修复方案
方案一:完全用Formik管理状态(推荐)
去掉自己的useState,直接用Formik提供的setFieldValue方法来更新parentId,这样表单状态完全由Formik统一管理,避免状态不一致的问题:
修改后的完整代码:
import React, {FC} from "react"; import {Box, Button, Stack, SxProps, TextField} from "@mui/material"; import {useFormik} from "formik"; // own modules import Catalog from "../catalog/Catalog"; const CreateCategory: FC = (sx?: SxProps) => { const formik = useFormik({ initialValues: { name: "", label: "", parentId: 0 }, onSubmit: (values, {setSubmitting}) => { console.log("values: ", values); // 现在parentId会正确更新 // ...other code... setSubmitting(false); } }); return ( <Box sx={{ display: "grid", justifyContent: "flex-start", gridTemplateColumns: "1fr 1fr 1fr 1fr", gap: "2rem", "> *": { gridColumn: "span 2" }, ...sx }} component="form" onSubmit={formik.handleSubmit} > <TextField error={!!(formik.errors.name && formik.touched.name)} name="name" onChange={formik.handleChange} value={formik.values.name} label="Name (technical - in Latin)" variant="outlined" helperText={formik.errors.name} /> <TextField error={!!(formik.errors.label && formik.touched.label)} name="label" onChange={formik.handleChange} value={formik.values.label} label="label(for users)" variant="outlined" helperText={formik.errors.label} /> <Stack direction="row" spacing={2}> <TextField sx={{flexGrow: 1}} error={!!(formik.errors.parentId && formik.touched.parentId)} name="parentId" value={formik.values.parentId} label="parentId" variant="outlined" helperText={formik.errors.parentId} // 加上onChange确保手动输入也能同步Formik状态 onChange={formik.handleChange} /> {/* 用Formik的setFieldValue更新parentId */} <Catalog onClickOverload={(id) => formik.setFieldValue("parentId", id)} /> <Button sx={{height: "100%", width: "max-content"}} onClick={() => formik.setFieldValue("parentId", 0)} > Set parent (0) </Button> </Stack> <Button sx={{gridColumnStart: 2, gridColumnEnd: 4}} type="submit" disabled={formik.isSubmitting} variant="outlined"> Create </Button> </Box> ) } export default CreateCategory;
方案二:同步useState和Formik状态(不推荐,多状态维护易出错)
如果一定要保留自己的parentId状态,需要在parentId变化时同步更新Formik的值,同时给TextField加上onChange事件同步回自己的状态:
// 先导入useEffect import React, {FC, useState, useEffect} from "react"; // 在组件内部添加useEffect同步状态 useEffect(() => { formik.setFieldValue("parentId", parentId); }, [parentId, formik]); // 然后修改TextField的onChange属性 <TextField // ...其他属性不变 onChange={(e) => { const value = Number(e.target.value); setParentId(value); }} />
为什么之前的代码不行?
Formik的initialValues是组件初始化时的快照,后续外部状态变化不会自动触发Formik内部values的更新。你只更新了自己的parentId状态,但Formik的表单状态根本没收到这个变化通知,所以提交时还是会使用初始化时的0值。
内容的提问来源于stack exchange,提问作者xNevrroXx
相关产品推荐
相关产品推荐

