reactjsredux-toolkitreact-leaflet-v3

Redux Toolkit `useAppSelector` value causes render when it isn't updated


Background

The title may be confusing, but to explain, I am using slices with Redux Toolkit in order to manage state. These slices hold various state values for my map application. Specifically, two that I'm having issues with renders is a clicked position (called focusedPosition and the mouse coordinates, mouseCoords). Each of these slices hold a lat and lng value. Using react-leaflet, I have map events that update the focusedPosition whenever the user clicks the map in order to display a popup. I also have an event to capture the mouseCoords to display in the corner of the map. However, for some reason, any component that is subscribed to focusedPosition updates re-render on mouse movement -- even though it is not subscribed to mouseCoords. This causes multiple issues to include performance issues as well as map popups to flicker as they are constantly re-rendering whenever the mouse is moved. If I comment out the mousemove event in react-leaflet, the issue stops as that value is no longer updating, but that's not an option as I really need those mouse coordinates to be captured.

How can I determine why these two values are being linked somehow and how can I fix this?

Applicable code is below, and a Code Sandbox

store.ts
export const store = configureStore({
  reducer: {
    focusedPosition: focusedPositionReducer,
    mouseCoords: mouseCoordsReducer,
  }
})
// Export AppDispatch
// Export RootState
// Export AppThunk
focusedPositionSlice.tsx
interface FocusedPositionState {
  lat: number | null
  lng: number | null
}
const initialState: FocusedPositionState = {
  lat: null,
  lng: null,
}
export const focusedPositionSlice = createSlice({
  name: 'focusedPosition',
  initialState,
  reducers: {
    clearFocusedPosition: state => {
      state.lat = null
      state.lng = null
    },
    setFocusedPosition: (state, action: PayloadAction<FocusedPositionState>) => {
      state.lat = action.payload.lat
      state.lng = action.payload.lng
    }
  }
})
// Export actions
// Export getFocusedPosition selector
// Export default reducer
mouseCoordsSlice.tsx
interface MouseCoordsState {
  lat: number
  lng: number
}
const initialState: MouseCoordsState = {
  lat: 0,
  lng: 0,
}
export const mouseCoordsSlice = createSlice({
  name: 'mouseCoords',
  initialState,
  reducers: {
    setMouseCoords: (state, action: PayloadAction<MouseCoordsState>) => {
      state.lat = action.payload.lat
      state.lng = action.payload.lng
    }
  }
})
// Export actions
// Export getMouseCoords selector
// Export default reducer

Solution

  • Your getFocusedPosition seletor creates a new object on each call to the reducer.

    Since react-redux rerenders when oldSelectorResult !=== newSelectorResult and these objects are not referentially equal, that will cause a rerender.

    You could either select the full slice state (at the risk of oversbscribing if in the future you add more props)

    export const getFocusedPosition = (state: RootState) => state.focusedPosition;
    

    or create a memoized selector that only returns a new object when an input value changes (see https://redux.js.org/recipes/computing-derived-data):

    export const getFocusedPosition = createSelector(
      state => state.focusedPosition.lat,
      state => state.focusedPosition.lng,
      (lat, lng) => ({ lat, lng })
    )
    

    or you just subscribe to both values individually:

    const lat = useAppSelector(state => state.focusedPosition.lat)
    const lng = useAppSelector(state => state.focusedPosition.lng)
    

    All of this is discussed further in the useSelector documentation: https://react-redux.js.org/api/hooks#equality-comparisons-and-updates