Компоненты, которые прямо или косвенно вызывают useContext
через пользовательский хук, будут рендериться каждый раз, когда изменяется значение провайдера.
Вы можете вызвать useContext
из контейнера и позволить контейнеру отобразить чистый компонент или Пусть контейнер вызывает функциональный компонент, который запоминается с помощью useMemo.
В приведенном ниже примере значение счетчика меняется каждые 100 миллисекунд, но, поскольку useMyContext
возвращает Math.floor(count / 10)
, компоненты будут отображаться только один раз в секунду. Контейнеры будут «рендериться» каждые 100 миллисекунд, но они будут возвращать один и тот же jsx 9 из 10 раз.
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, возвращает что-то отличное от того, которое было в прошлый раз run и все функции, переданные useSelector, будут работать при изменении избыточного хранилища. Так что здесь вам не нужно разделять контейнер и представление для предотвращения повторного рендеринга.
Компоненты также будут повторно рендериться, если их родительский объект рендерится и передает им другие реквизиты или если родительский рендеринг воспроизводится, а компонент является функциональным компонентом (функциональные компоненты всегда перерисовываются). Но так как в примере кода родительский элемент (App) никогда не рендерится, мы можем определить контейнеры как функциональные компоненты (не нужно переносить в React.memo).
Ниже приведен пример того, как вы можете создать подключенный компонент аналогичен реагирующему соединению с редуксом, но вместо этого он подключается к контексту:
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>