如何避免自定义Input onChange时重渲染同时通知父组件?
解决自定义输入组件不必要重渲染的问题
我希望实现一个自定义输入组件,要求输入值变化时组件自身不触发重渲染,但能通知父组件更新状态。目前的代码中,尽管父组件通过useCallback传递了回调函数,但每次输入时子组件依然会重渲染;移除该回调prop后则不会出现这个问题,请问解决方案是什么?
问题原因
- 父组件调用
setValue更新状态时会触发自身重渲染,React默认规则下,父组件重渲染会导致所有子组件跟着重渲染,无论props是否变化。 - 虽然用
useCallback缓存了changeFunction的引用,但子组件没有做任何阻止不必要重渲染的优化,所以父组件重渲染时子组件仍会重新执行。 - 子组件中的
useEffect未设置依赖数组,每次渲染都会执行,这也直观证明了组件一直在重复渲染。
解决方案
1. 用React.memo包裹子组件
React.memo是高阶组件,会对组件的props做浅比较,只有当props发生实际变化时才会触发组件重渲染。因为父组件通过useCallback保证了changeFunction的引用稳定,所以子组件的props不会变化,也就不会触发不必要的重渲染。
2. 优化子组件的useEffect
给useEffect添加空依赖数组,让它仅在组件挂载时执行一次,避免每次渲染都打印日志,同时符合性能优化的原则。
3. 可选:用useCallback包裹子组件的change函数
将子组件内部的change函数用useCallback包裹,可以避免每次渲染时创建新的函数实例。虽然在React.memo的加持下这一步不是必须的,但能进一步优化组件性能。
修改后的代码
TypeScript版子组件
import { useRef, useEffect, ChangeEvent, useCallback, memo } from "react"; type Props = { changeFunction?: (e: ChangeEvent<HTMLInputElement>) => void; }; const TestInput = memo(({ changeFunction }: Props) => { const inputRef = useRef<HTMLInputElement>(null); useEffect(() => { console.log("Input mount"); }, []); // 空依赖数组,仅挂载时执行 const change = useCallback((e: ChangeEvent<HTMLInputElement>) => { console.log("Input change", e.target.value); if (inputRef.current) inputRef.current.value = e.target.value; if (changeFunction) changeFunction(e); }, [changeFunction]); // 依赖changeFunction,保证函数引用稳定 return ( <input ref={inputRef} type="search" id="test" name="test" autoComplete="off" onChange={change} /> ); }); export default TestInput;
父组件(无需修改)
import { useCallback, useState, ChangeEvent } from "react"; import TestInput from "./Test"; function TestParent() { const [value, setValue] = useState(""); const change = useCallback( (e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value), [] ); return ( <div> <p>Value: {value}</p> <TestInput changeFunction={change} /> </div> ); } export default TestParent;
无TypeScript版完整示例
const { useState, useEffect, useCallback, useRef, memo } = React; const TestInput = memo(({ changeFunction }) => { const inputRef = useRef(null); useEffect(() => { console.log("Input mount"); }, []); const change = useCallback((e) => { console.log("Input change", e.target.value); if (inputRef.current) inputRef.current.value = e.target.value; if (changeFunction) changeFunction(e); }, [changeFunction]); return ( <input ref={inputRef} type="search" id="test" name="test" autoComplete="off" onChange={change} /> ); }); function TestParent() { const [value, setValue] = useState(""); const change = useCallback( (e) => setValue(e.target.value), [] ); return ( <div> <p>Value: {value}</p> <TestInput changeFunction={change} /> </div> ); } const root = ReactDOM.createRoot(document.getElementById("root")); root.render(<TestParent />);
<div id="root"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
内容的提问来源于stack exchange,提问作者Sarfo
相关产品推荐
相关产品推荐

