我想找到一种避免使用useContext
时不必要的重新渲染的方法。我创建了一个玩具沙箱,您可以在这里找到:https://codesandbox.io/s/eager-mclean-oupkx,以演示该问题。如果打开开发者控制台,您将每秒看到console.log
发生。
在此示例中,我有一个上下文,该上下文仅每1秒增加一个计数器。然后,我有一个消耗它的组件。我不希望计数器的每个刻度都导致组件上的重新渲染。例如,我可能只想每隔10个滴答声更新一次组件,或者我想要一些其他随着滴答声更新的组件。我尝试创建一个用于包装useContext
的钩子,然后返回一个静态值,希望这样做不会阻止重新渲染。
我能想到的解决此问题的最佳方法是将Component
包裹在HOC中,该HOC通过count
消耗useContext
,然后将值传递给Component
作为道具。这似乎是完成某些本应简单的事情的一种round回的方式。
有更好的方法吗?
答案 0 :(得分:0)
通过自定义钩子直接或间接调用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的所有函数将在redux存储更改时运行,才会重现调用useSelector
的组件。因此,在这里您不必拆分容器和演示文稿即可防止重新渲染。
如果组件的父级重新渲染并通过不同的prop传递给它们,或者如果父级重新渲染并且组件是功能性组件(功能性组件始终会重新渲染),则组件也会重新渲染。但是,由于在代码示例中,父代(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>