You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Formik表单onChange无回调失效及setState错误问题咨询

问题解决:Formik表单中避免bad setState错误并实时控制按钮禁用

问题描述

现有代码触发了bad setState错误,需求是在表单字段未填写完整时禁用按钮:

  • 希望onChange中的代码无需使用()=>回调就能立即执行
  • 使用箭头函数回调时,onChange的状态更新总是滞后一次输入变化

错误代码片段:

<FormControl
  onChange={
    Object.keys(formik.errors).length === 0 &&
    Object.keys(formik.touched).length >= 2
      ? setErrorState(false, formik.values)
      : setErrorState(true)
  }
>

问题原因

  1. 直接执行函数而非传递回调:当前代码中onChange的值是表达式的执行结果(直接调用了setErrorState),这会在组件渲染时就触发setState,违反React渲染期间不能更新状态的规则,导致bad setState错误。
  2. 状态滞后问题:使用箭头函数时,若依赖的formik.errors/formik.touched是闭包捕获的旧值,会因为Formik状态更新时机导致回调拿到的不是最新状态,出现滞后。

解决方案

方案1:利用Formik内置状态监听,用useEffect实时同步

不需要在FormControl上绑定onChange,而是通过useEffect监听Formik的核心状态变化,实时更新Redux状态和本地状态:

import { useEffect } from "react";

const ShippingInformation = () => {
  const dispatch = useDispatch();
  const [formStateChanged, setFormStateChanged] = useState(false);

  const setErrorState = (errorState, data) => {
    if (errorState === false) {
      dispatch(setShippingAddress(data));
    }
    if (
      (!formStateChanged && !errorState) ||
      (formStateChanged && errorState)
    ) {
      return;
    } else {
      setFormStateChanged(errorState);
      dispatch(setShippingAddressError(errorState));
    }
  };

  return (
    <Formik
      initialValues={{ address: "", postalCode: "", city: "", country: "" }}
      validationSchema={Yup.object({
        // 保持原验证规则不变
        address: Yup.string()
          .required("This field is required.")
          .min(2, "This address is too short."),
        postalCode: Yup.string()
          .required("This field is required.")
          .min(2, "This postal code is too short."),
        city: Yup.string()
          .required("This field is required.")
          .min(2, "This city name is is too short."),
        country: Yup.string()
          .required("This field is required.")
          .min(2, "This country name is too short."),
      })}
    >
      {(formik) => {
        // 监听Formik的errors、touched、values变化
        useEffect(() => {
          const hasNoErrors = Object.keys(formik.errors).length === 0;
          const hasEnoughTouched = Object.keys(formik.touched).length >= 2;
          const isValid = hasNoErrors && hasEnoughTouched;

          setErrorState(!isValid, formik.values);
        }, [formik.errors, formik.touched, formik.values, formStateChanged]);

        return (
          <VStack as="form">
            {/* 移除FormControl上的onChange */}
            <FormControl>
              <TextField
                name="address"
                placeholder="Street Address"
                label="Street Address"
              />
              <Flex>
                <Box flex="1" mr="10">
                  <TextField
                    name="postalCode"
                    placeholder="Postal Code"
                    label="Postal Code"
                  />
                </Box>
                <Box flex="2">
                  <TextField name="city" placeholder="City" label="City" />
                </Box>
              </Flex>
              <TextField name="country" placeholder="Country" label="Country" />
            </FormControl>
            {/* 示例:根据Formik状态控制按钮禁用 */}
            <Button
              type="submit"
              disabled={Object.keys(formik.errors).length > 0 || Object.keys(formik.touched).length < 2}
              mt="20px"
            >
              确认地址
            </Button>
            {/* 原Shipping Method部分保持不变 */}
            <Box width="100%" height="180px" pr="5px">
              <Heading fontSize="2xl" fontWeight="extrabold" mb="10px">
                Shipping Method
              </Heading>
              <RadioGroup
                defaultValue="false"
                onChange={(event) => {
                  dispatch(setExpress(event));
                }}
              >
                <Stack
                  direction={{ base: "column", lg: "row" }}
                  align={{ lg: "flex-start" }}
                >
                  <Stack pr="10" spacing={{ base: "8px", md: "10px" }} flex="1.5">
                    <Box>
                      <Radio value="true">
                        <Text fontWeight="bold">Express Shipping $14.99</Text>
                        <Text>Shipped within 24 hours</Text>
                      </Radio>
                    </Box>
                    <Stack spacing="6px">
                      <Text>Express</Text>
                    </Stack>
                  </Stack>
                  <Radio value="false">
                    <Tooltip label="Free shipping for all orders over $1,000.">
                      <Box>
                        <Text fontWeight="bold">Standard Shipping $4.99</Text>
                        <Text>Shipped within two to three business days</Text>
                      </Box>
                    </Tooltip>
                  </Radio>
                </Stack>
              </RadioGroup>
            </Box>
          </VStack>
        );
      }}
    </Formik>
  );
};

方案2:直接在按钮上使用Formik状态判断禁用

如果不需要额外的formStateChanged本地状态,可直接在按钮的disabled属性中根据Formik的实时状态判断,完全省去自定义的setErrorState逻辑:

<Button
  type="submit"
  disabled={
    Object.keys(formik.errors).length > 0 || 
    Object.keys(formik.touched).length < 2
  }
  onClick={() => {
    // 提交时直接dispatch表单值
    dispatch(setShippingAddress(formik.values));
  }}
  mt="20px"
>
  确认地址
</Button>

关键说明

  • Formik的errors、touched、values都是实时更新的状态,通过useEffect监听这些值的变化,能确保每次输入后立即触发状态同步,不会出现滞后。
  • 永远不要在React组件的渲染阶段直接调用setState或类似状态更新函数,必须通过事件回调或useEffect等生命周期钩子触发。

内容的提问来源于stack exchange,提问作者user19703227

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 11:31:05