使用 React 钩子 useContext 避免不必要的重新渲染
Posted
技术标签:
【中文标题】使用 React 钩子 useContext 避免不必要的重新渲染【英文标题】:Avoid unnecessary re-renders with React hook useContext 【发布时间】:2020-07-26 17:32:09 【问题描述】:我想找到一种在使用useContext
时避免不必要的重新渲染的方法。我创建了一个玩具沙箱,你可以在这里找到它:https://codesandbox.io/s/eager-mclean-oupkx 来演示这个问题。如果你打开开发控制台,你会看到console.log
每秒都在发生。
在这个例子中,我有一个简单地每 1 秒递增一个计数器的上下文。然后我有一个消耗它的组件。我不希望计数器的每个刻度都导致组件重新渲染。例如,我可能只想每 10 个滴答声更新我的组件,或者我可能想要一些其他组件随着每个滴答声更新。我尝试创建一个钩子来包装useContext
,然后返回一个静态值,希望这样可以防止重新渲染,但无济于事。
我能想到的解决此问题的最佳方法是将我的 Component
包装在一个 HOC 中,它通过 useContext
消耗 count
,然后将值作为道具传递给 Component
.这似乎是一种有点迂回的方式来完成应该很简单的事情。
有没有更好的方法来做到这一点?
【问题讨论】:
感觉就像你试图通过这个超级简化的演示来破坏系统。 React 钩子运行每个渲染,并且功能组件在状态或道具更新时重新渲染。我在这里同意,但如果您不希望组件在每次更新上下文时重新渲染,那么将其分解并作为道具传递。memo
的反应将是帮助尝试减少无关渲染的 HOC(尽管根据定义,它们根本不是无关的,因为它们完全按照设计进行渲染!!)
【参考方案1】:
通过自定义钩子直接或间接调用useContext
的组件将在每次提供者的值发生变化时重新渲染。
你可以从容器中调用useContext
,让容器渲染一个纯组件,或者让容器调用一个用useMemo记忆的函数式组件。
在下面的示例中,计数每 100 毫秒更改一次,但因为 useMyContext
返回 Math.floor(count / 10)
,组件将仅每秒渲染一次。容器将每 100 毫秒“渲染”一次,但它们会在 10 次中返回相同的 jsx 9。
const
useEffect,
useMemo,
useState,
useContext,
memo,
= React;
const MyContext = React.createContext( count: 0 );
function useMyContext()
const count = useContext(MyContext);
return Math.floor(count / 10);
// simple context that increments a timer
const Context = ( children ) =>
const [count, setCount] = useState(0);
useEffect(() =>
const i = setInterval(() =>
setCount((c) => c + 1);
, [100]);
return () => clearInterval(i);
, []);
const value = useMemo(() => ( count ), [count]);
return (
<MyContext.Provider value=value>
children
</MyContext.Provider>
);
;
//pure component
const MemoComponent = memo(function Component( count )
console.log('in memo', count);
return <div>MemoComponent count</div>;
);
//functional component
function FunctionComponent( count )
console.log('in function', count);
return <div>Function Component count</div>;
// container that will run every time context changes
const ComponentContainer1 = () =>
const count = useMyContext();
return <MemoComponent count=count />;
;
// second container that will run every time context changes
const ComponentContainer2 = () =>
const count = useMyContext();
//using useMemo to not re render functional component
return useMemo(() => FunctionComponent( count ), [
count,
]);
;
function App()
console.log('App rendered only once');
return (
<Context>
<ComponentContainer1 />
<ComponentContainer2 />
</Context>
);
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
使用 React-redux useSelector,只有当传递给 useSelector 的函数返回不同于上次运行时的内容时,才会重新渲染调用 useSelector
的组件,并且传递给 useSelector 的所有函数都将在 redux 存储更改时运行。所以在这里你不必拆分容器和展示来防止重新渲染。
如果父组件重新渲染并传递不同的 props,或者父组件重新渲染并且组件是功能组件(功能组件总是重新渲染),组件也会重新渲染。但是由于在代码示例中父级(App)从不重新渲染,我们可以将容器定义为功能组件(无需包装在 React.memo 中)。
以下是如何创建类似于 react redux connect 的连接组件的示例,但它会连接到上下文:
const
useMemo,
useState,
useContext,
useRef,
useCallback,
= React;
const createSelector = Reselect;
const MyContext = React.createContext( count: 0 );
//react-redux connect like function to connect component to context
const connect = (context) => (mapContextToProps) =>
const select = (selector, state, props) =>
if (selector.current !== mapContextToProps)
return selector.current(state, props);
const result = mapContextToProps(state, props);
if (typeof result === 'function')
selector.current = result;
return select(selector, state, props);
return result;
;
return (Component) => (props) =>
const selector = useRef(mapContextToProps);
const contextValue = useContext(context);
const contextProps = select(
selector,
contextValue,
props
);
return useMemo(() =>
const combinedProps =
...props,
...contextProps,
;
return (
<React.Fragment>
<Component ...combinedProps />
</React.Fragment>
);
, [contextProps, props]);
;
;
//connect function that connects to MyContext
const connectMyContext = connect(MyContext);
// simple context that increments a timer
const Context = ( children ) =>
const [count, setCount] = useState(0);
const increment = useCallback(
() => setCount((c) => c + 1),
[]
);
return (
<MyContext.Provider value= count, increment >
children
</MyContext.Provider>
);
;
//functional component
function FunctionComponent( count )
console.log('in function', count);
return <div>Function Component count</div>;
//selectors
const selectCount = (state) => state.count;
const selectIncrement = (state) => state.increment;
const selectCountDiveded = createSelector(
selectCount,
(_, divisor) => divisor,
(count, divisor ) => Math.floor(count / divisor)
);
const createSelectConnectedContext = () =>
createSelector(selectCountDiveded, (count) => (
count,
));
//connected component
const ConnectedComponent = connectMyContext(
createSelectConnectedContext
)(FunctionComponent);
//app is also connected but won't re render when count changes
// it only gets increment and that never changes
const App = connectMyContext(
createSelector(selectIncrement, (increment) => (
increment,
))
)(function App( increment )
const [divisor, setDivisor] = useState(0.5);
return (
<div>
<button onClick=increment>increment</button>
<button onClick=() => setDivisor((d) => d * 2)>
double divisor
</button>
<ConnectedComponent divisor=divisor />
<ConnectedComponent divisor=divisor * 2 />
<ConnectedComponent divisor=divisor * 4 />
</div>
);
);
ReactDOM.render(
<Context>
<App />
</Context>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
【讨论】:
以上是关于使用 React 钩子 useContext 避免不必要的重新渲染的主要内容,如果未能解决你的问题,请参考以下文章
在使用数据库数据更新上下文后,React 'useContext' 钩子不会重新渲染
如何将 React 钩子(useContext、useEffect)与 Apollo 反应钩子(useQuery)结合起来
React TS useContext useReducer 钩子