I have an application that uses a mapbox map and I am trying to allow for an adjustment of the viewport from another, separate card component. The way I went about this was creating a viewport context and passing the viewport and the function that sets it to both the map and the card component.
This works, but I'm running into an issue with rerendering. Whenever the user is scrolling around on the map, the viewport is constantly changing and thus everything inside the Viewport Provider is constantly rerendering. A simplified version of the code is below
App
<ViewportProvider>
*Container that holds both the map and the sidebar that renders the card components*
</ViewportProvider>
ViewportProvider
import React from "react";
const INITIAL_STATE = {
longitude: 15,
latitude: 25,
zoom: 1
};
export const ViewportContext = React.createContext();
export const ViewportProvider = (props) => {
const [viewport, setViewport] = React.useState(INITIAL_STATE);
return (
<ViewportContext.Provider
value={{
viewport,
setViewport,
}}
{...props}
/>
);
}
Map
import React, { useContext } from "react";
import ReactMapGL from "react-map-gl";
import { ViewportContext } from "./ViewportProvider";
const { viewport, setViewport } = useContext(ViewportContext);
<Map onViewportChange={nextViewport => setViewport(nextViewport)} {...viewport}/>
Card
const {setViewport} = useContext(ViewportContext)
<Card onClick={setViewport(...newValue)}/>
How can I stop the excessive rerendering when the user is moving around the map without losing the ability for the user to adjust the viewport from the card component?
By putting viewport
and setViewport
in the same context, you are forcing the sidebar to re-render each time the viewport changes, because it uses useContext(ViewportContext)
— even though it only uses the setViewport
part of the context. There are a few options:
useContext(ViewportContext)
into a sub-component, so that only the sub-component, not the whole sidebar, renders whenever the viewport is updated.viewport
and setViewport
in separate contexts. This would mean two createContext()
calls, but you would still keep a single ViewportProvider component that renders both providers, e.g. const [viewport, setViewport] = useState(); return <Context1.Provider value={viewport}><Context2.Provider value={setViewport}> ...
. Then, your sidebar can useContext
only for the context it needs, and will not re-render when the other one changes.useContextSelector
to replace const {setViewport} = useContext(ViewportContext)
with const setViewport = useContextSelector(ViewportContext, (ctx) => ctx.setViewport)
.(By the way, in your code as written, I would recommend you wrap the value
you pass to ViewportContext.Provider
in a useMemo(() => ({viewport, setViewport}), [viewport, setViewport])
, so that it doesn't trigger an update when the ViewportProvider is re-rendered if the viewport hasn't changed.)