React兄弟组件传值时如何避免不必要重渲染?
问题描述
尝试从子组件向兄弟组件更新状态时,所有组件持续重渲染。推测是状态更新需通过父组件传递导致整页重渲染。仅希望Child Component 1更新数据时,Child Component 2重渲染,但目前所有组件都在重渲染,还导致部分抽屉关闭,用户体验糟糕。已尝试用React Context解决但未成功,求优化建议。
最小复现代码如下:
React Context 创建文件
import React from "react"; interface RangeHighlightsContextProps { setRangeHighlights: (highlights: number[]) => void; rangeHighlights: number[]; } export const RangeHighlightsContext = React.createContext<RangeHighlightsContextProps>({ setRangeHighlights: () => {}, rangeHighlights: [], });
父组件
import { RangeHighlightsContext } from "./contexts/RangeHighlightContext"; const [rangeHighlights, setRangeHighlights] = useState<number[]>([]); // ...其他代码 <RangeHighlightsContext.Provider value={{ setRangeHighlights, rangeHighlights }}> <ChildComponent1 /> <ChildComponent2 /> </RangeHighlightsContext.Provider>
Child Component 1
function ChildComponent1(){ const { setRangeHighlights } = useContext(RangeHighlightsContext); function handleRangeHighlightsChange(highlights: number[]) { setRangeHighlights(highlights); } return( <div onClick={(e) => { e.stopPropagation(); handleRangeHighlightsChange([1,2,3]); }}>Click Here</div> ) }
Child Component 2
function ChildComponent2(){ const newHighlights = useContext(RangeHighlightsContext); return( <div>{newHighlights.rangeHighlights}</div> ) }
子组件中抽屉状态的设置方式:
const [drawerState, setDrawerState] = useState("none"); const [drawerOpen, setDrawerOpen] = useState(false); // 补充缺失的状态定义 useEffect(() => { if (!drawerOpen) { setDrawerState("none"); } if (drawerOpen) { console.log("opening"); setDrawerState("block"); } }, [drawerOpen]); function handleDrawer() { setDrawerOpen((drawerOpen) => !drawerOpen); }
优化方案
1. 缓存Context Provider的value
父组件每次渲染时,都会创建新的{ setRangeHighlights, rangeHighlights }对象,导致Context Provider的value变化,所有消费该Context的组件都会重渲染。用useMemo缓存这个value,只有当rangeHighlights变化时才更新:
// 父组件中 const contextValue = useMemo(() => ({ setRangeHighlights, rangeHighlights }), [rangeHighlights]); // setRangeHighlights是useState返回的稳定函数,无需加入依赖 <RangeHighlightsContext.Provider value={contextValue}> <ChildComponent1 /> <ChildComponent2 /> </RangeHighlightsContext.Provider>
2. 拆分Context(进一步优化)
如果只需要Child1调用更新函数、Child2读取状态,可以拆分两个Context:一个存状态,一个存更新函数。这样消费更新函数的组件(Child1)不会因为状态变化而重渲染:
拆分后的Context文件
import React from "react"; // 状态Context export const RangeHighlightsStateContext = React.createContext<number[]>([]); // 更新函数Context export const RangeHighlightsDispatchContext = React.createContext<(highlights: number[]) => void>(() => {});
父组件修改
import { RangeHighlightsStateContext, RangeHighlightsDispatchContext } from "./contexts/RangeHighlightContext"; const [rangeHighlights, setRangeHighlights] = useState<number[]>([]); // 缓存更新函数(可选,setRangeHighlights本身稳定) const dispatch = useCallback(setRangeHighlights, []); <RangeHighlightsStateContext.Provider value={rangeHighlights}> <RangeHighlightsDispatchContext.Provider value={dispatch}> <ChildComponent1 /> <ChildComponent2 /> </RangeHighlightsDispatchContext.Provider> </RangeHighlightsStateContext.Provider>
Child1修改
function ChildComponent1(){ const setRangeHighlights = useContext(RangeHighlightsDispatchContext); function handleRangeHighlightsChange(highlights: number[]) { setRangeHighlights(highlights); } return( <div onClick={(e) => { e.stopPropagation(); handleRangeHighlightsChange([1,2,3]); }}>Click Here</div> ) }
Child2修改
function ChildComponent2(){ const rangeHighlights = useContext(RangeHighlightsStateContext); return( <div>{rangeHighlights}</div> ) }
3. 用React.memo包裹不需要重渲染的组件
对于抽屉所在的组件(以及其他不需要随rangeHighlights变化的组件),用React.memo包裹,避免不必要的重渲染:
const DrawerComponent = React.memo(function DrawerComponent() { const [drawerState, setDrawerState] = useState("none"); const [drawerOpen, setDrawerOpen] = useState(false); useEffect(() => { setDrawerState(drawerOpen ? "block" : "none"); }, [drawerOpen]); function handleDrawer() { setDrawerOpen(prev => !prev); } return ( // 抽屉JSX ); });
4. 优化抽屉状态逻辑(可选)
抽屉的drawerState可以直接由drawerOpen推导,无需额外状态和useEffect,减少不必要的更新:
const DrawerComponent = React.memo(function DrawerComponent() { const [drawerOpen, setDrawerOpen] = useState(false); function handleDrawer() { setDrawerOpen(prev => !prev); } return ( <div style={{ display: drawerOpen ? "block" : "none" }}> {/* 抽屉内容 */} </div> ); });
内容的提问来源于stack exchange,提问作者deadant88
相关产品推荐
相关产品推荐

