如何阻止Formik内组件在表单字段更新时触发非必要重渲染
React.memo 只能拦截props变更触发的重渲染,对Context更新导致的重渲染完全无效。Formik全量表单状态是通过React Context向下传递的,只要Formik子树内的组件调用了useFormikContext、useField这类Formik Hook,哪怕没给组件传任何props,任意表单字段更新带来的Context值变化,都会直接触发组件重渲染。enableReinitialize={false}只控制初始值变更时是否重置表单,和正常字段更新的重渲染逻辑没有关系,加了也不会生效。
以下方案都可以保留组件在Formik标签内部,按需选择即可:
方案1:useMemo缓存渲染节点(组件不依赖Formik状态时用)
如果Rerenderedcomponent完全不需要读取、修改Formik的任何状态和方法,直接在Formik的render逻辑里用useMemo缓存该组件的渲染结果,依赖设为空数组,Formik重渲染时会直接复用缓存节点,不会触发组件重渲染:
import { useMemo } from 'react'; import { Formik } from 'formik'; <Formik initialValues={{ show: false }} onSubmit={(values) => console.log(values)} > {({ values, setFieldValue }) => { // 仅在首次渲染时生成组件节点,后续直接复用 const staticComp = useMemo(() => <Rerenderedcomponent />, []); return ( <> <button name="showbtn" onClick={() => setFieldValue("show", true)} > 切换显示 </button> {values?.show ? <Text>Hello</Text> : null} {staticComp} </> ) }} </Formik>
方案2:拆分Context消费边界(组件需要用到Formik能力时用)
如果组件确实需要调用Formik的部分方法,不要在组件内部直接调用useFormikContext获取全量Context——全量Context每次表单更新都会生成新引用,必然触发重渲染。单独写一层memo化的包装组件,只把组件实际需要的、引用稳定的Formik方法(比如submitForm、resetForm这类Formik内部已经做过引用稳定处理的方法)作为props传入,React.memo浅比较props无变化时就不会触发重渲染:
import { useFormikContext } from 'formik'; // 包装层只订阅组件需要的固定属性 const MemoizedRerenderComp = React.memo(() => { const { submitForm } = useFormikContext(); return <Rerenderedcomponent onSubmit={submitForm} />; }); // Formik内直接使用包装后的组件即可 <Formik ...> {({ values, setFieldValue }) => ( <> {/* 其他表单元素 */} <MemoizedRerenderComp /> </> )} </Formik>
注意:不要从useFormikContext里解构values、touched、errors这类每次表单更新都会变引用的对象传入组件,否则props每次都是新引用,memo会直接失效。
方案3:从根源隔离API调用逻辑
组件重渲染时API重复调用,本质是请求逻辑写得有问题:不要把API请求直接写在组件函数顶层(组件每次重渲染都会执行顶层代码,必然重复触发请求),把请求逻辑挪到useEffect里,搭配正确的依赖数组,就可以和重渲染逻辑完全解耦,哪怕组件有意外重渲染,只要依赖不变就不会重复发请求:
import { useEffect, useState, useCallback } from 'react'; function Rerenderedcomponent() { const [apiData, setApiData] = useState(null); // 仅在组件首次挂载时拉取API数据 useEffect(() => { const fetchData = async () => { const res = await fetch('/your/api/path'); setApiData(await res.json()); }; fetchData(); }, []); const callingAPI = useCallback(()=> response,[]); return <div>{/* 业务渲染逻辑 */}</div>; } export default React.memo(Rerenderedcomponent);
你贴的示例代码有两个基础写法错误,也可能触发额外的异常重渲染,需要先修正:
- Formik的初始值属性是
initialValues(结尾带s),不是initialValue - React事件名要用小驼峰格式
onClick,setFieldValue是函数调用,要包在箭头函数里传给事件,不能直接写onClick={setFieldValue("show",true)},否则组件渲染阶段就会直接执行,触发非预期的状态更新。
内容的提问来源于stack exchange,提问作者vivek sharma

