1

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
c-mcb92
  • 170
  • 1
  • 9

1 Answers1

3

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

phry
  • 35,762
  • 5
  • 67
  • 81
  • Thank you!! I'm going to try this after work. I think that these are the only two slices in my store that the selector is like that. Everything else is a single variable `value` with an enum or value as its type. These are the only ones that return an object versus `state => state.SLICE.value` I'll try to have it set up like either the state interface as `value: { object literal }` or keep it like you have it as the selector just returns `state.focusedPosition` – c-mcb92 Oct 27 '21 at 12:04
  • Would you say changing it to hold `value: { lat, lng }` in the state and then returning `state.focusedPosition.value` is better convention, or like you have it, returning `state.focusedPosition` is better? – c-mcb92 Oct 27 '21 at 12:43
  • Personally I would just go with the "use `useAppSelector` twice approach" to be honest, that is the most simple you can do. I would not introduce any changes to state structure. – phry Oct 27 '21 at 16:04