Search code examples
javascriptreactjsreact-hooksmapboxviewport

Preventing excessive rerendering when using mapbox and react context


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?


Solution

  • 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:

    • Reduce the performance impact of re-rendering by simply moving useContext(ViewportContext) into a sub-component, so that only the sub-component, not the whole sidebar, renders whenever the viewport is updated.
    • If you only need one-way updates (sidebar button can set the map's viewport, but no other components need to know when the user moves around the map), then you could remove the setViewport call from the map.
    • Put 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.
    • Use something like useContextSelector to replace const {setViewport} = useContext(ViewportContext) with const setViewport = useContextSelector(ViewportContext, (ctx) => ctx.setViewport).
    • If you're considering broader architectural changes, libraries like React Redux or Recoil could provide alternative designs that would avoid this problem.

    (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.)