Material UI TextField在Formik中引发Wrapper组件过度渲染如何解决?
我在React项目中使用Formik,包含以下组件:
Wrapper组件
const Wrapper = ({ children, id }) => { console.log("re-render ", id); return <div>{children}</div>; };
Form组件
<Formik initialValues={{ title: "", description: "", }} validationSchema={Yup.object().shape({ title: Yup.string().required("Title is required"), description: Yup.string().required("Description"), })} onSubmit={(values, { setSubmitting }) => { // ...submit stuff }} > {({ handleChange, handleSubmit, isSubmitting, values }) => ( <form onSubmit={handleSubmit} encType="multipart/form-data"> <Wrapper id="title"> <TextField variant="outlined" fullWidth label="title" name="title" margin="dense" value={values.title} onChange={handleChange} /> </Wrapper> <Wrapper id="description"> <TextField variant="outlined" fullWidth label="description" name="description" margin="dense" value={values.description} onChange={handleChange} /> </Wrapper> <Button type="submit" > Submit </Button> </form> )} </Formik>
问题:每当我在TextField中输入内容时,Wrapper组件会多次渲染,甚至未包含当前输入TextField的其他Wrapper组件也会出现过度渲染。实际项目中Wrapper组件更复杂,导致输入时出现卡顿,该如何解决?
原因分析
每次输入时Formik的values对象会更新,触发Formik内部的重新渲染,而表单结构直接定义在Formik的渲染回调中,这个回调每次都会重新执行,导致所有Wrapper组件(包括未关联当前输入的)都跟着重新渲染。另外,handleChange这类Formik提供的方法每次渲染都会生成新的引用,也会间接触发子组件的重新渲染。
解决方案
1. 使用Formik的Field组件精准控制渲染
Formik的Field组件内部做了优化,只会在对应字段变化时重新渲染,能缩小组件的渲染范围:
// 封装可复用的TextField组件 const FormTextField = ({ field, form, ...props }) => ( <TextField variant="outlined" fullWidth margin="dense" {...field} {...props} /> ); // 改写Form组件 <Formik initialValues={{ title: "", description: "" }} validationSchema={Yup.object().shape({ title: Yup.string().required("Title is required"), description: Yup.string().required("Description"), })} onSubmit={(values, { setSubmitting }) => { // ...submit stuff }} > {({ handleSubmit, isSubmitting }) => ( <form onSubmit={handleSubmit} encType="multipart/form-data"> <Wrapper id="title"> <Field name="title" component={FormTextField} label="title" /> </Wrapper> <Wrapper id="description"> <Field name="description" component={FormTextField} label="description" /> </Wrapper> <Button type="submit">Submit</Button> </form> )} </Formik>
这样只有当前输入的字段对应的Field和Wrapper会重新渲染,其他字段的组件不会触发渲染。
2. 用React.memo包裹Wrapper组件
给Wrapper组件添加React.memo,让它只在props变化时才重新渲染:
import { memo } from 'react'; const Wrapper = memo(({ children, id }) => { console.log("re-render ", id); return <div>{children}</div>; });
注意:如果children是每次渲染都重新生成的元素,memo可能失效,需要结合其他优化方法配合使用。
3. 稳定handleChange等方法的引用
使用useFormikContext钩子在子组件中直接获取Formik方法,避免每次渲染生成新的函数引用:
// 在TextField组件内部获取Formik上下文 const FormTextField = ({ name, label }) => { const { handleChange, values } = useFormikContext(); return ( <TextField variant="outlined" fullWidth label={label} name={name} margin="dense" value={values[name]} onChange={handleChange} /> ); }; // 表单中使用封装后的组件 <Wrapper id="title"> <FormTextField name="title" label="title" /> </Wrapper>
这样handleChange的引用不会每次渲染都变化,配合React.memo的Wrapper就能有效减少不必要的渲染。
4. 拆分独立字段组件
把每个字段的Wrapper和TextField拆成独立的子组件,让每个组件只依赖自己需要的Formik状态:
const TitleField = () => { const { values, handleChange } = useFormikContext(); return ( <Wrapper id="title"> <TextField variant="outlined" fullWidth label="title" name="title" margin="dense" value={values.title} onChange={handleChange} /> </Wrapper> ); }; const DescriptionField = () => { const { values, handleChange } = useFormikContext(); return ( <Wrapper id="description"> <TextField variant="outlined" fullWidth label="description" name="description" margin="dense" value={values.description} onChange={handleChange} /> </Wrapper> ); }; // 改写Form组件 <Formik // ...其他配置 > {({ handleSubmit, isSubmitting }) => ( <form onSubmit={handleSubmit} encType="multipart/form-data"> <TitleField /> <DescriptionField /> <Button type="submit">Submit</Button> </form> )} </Formik>
每个独立字段组件只会在自己对应的values字段变化时重新渲染,不会受其他字段影响。
内容的提问来源于stack exchange,提问作者Justin Timbersaw

