I have created a custom React hook so that I can utilise the Basemap toggle view model logic in another section of my application. I am following a pattern similar to that which is described in this repo: https://github.com/rslibed/2023DS-build-a-custom-ui-for-api-widgets-bookmarks/tree/master/src/Compon...
The issue I am seeing is that the hook I create infinitely re-renders because the view model mutates the properties it takes in. In this case I specifically see the nextBasemap property is being altered when I call new BaseMapToggleVM.
The only fix I can see is that I clone some of these objects within the custom hook, so that I don't directly pass them into the viewmodel, but it really surprised me that the esri logic would be so gungho about directly mutating the values I passed in. In fact it seems to break a very standard ESlint rule: https://eslint.org/docs/latest/rules/no-param-reassign
Am I missing something here? Is this a bug or just something I need to deal with/work around?
import BasemapToggleVM from "@arcgis/core/widgets/BasemapToggle/BasemapToggleViewModel";
import React from "react";
import Handles from "@arcgis/core/core/Handles";
import { watch } from "@arcgis/core/core/reactiveUtils";
export function useBaseMapToggleModel({ ...props }: __esri.BasemapToggleViewModelProperties) {
const [basemapToggleVM, setBasemapToggleVm] =
React.useState<__esri.BasemapToggleViewModel | null>(null);
const [state, setState] = React.useState<BasemapToggleVM["state"]>("disabled");
React.useEffect(() => {
const basemapToggleModel = new BasemapToggleVM(props);
setBasemapToggleVm(basemapToggleModel);
return () => {
basemapToggleModel.destroy();
};
}, [props]);
React.useEffect(() => {
if (!basemapToggleVM) return () => {};
const handles = new Handles();
addHandlers({ basemapToggleVM, handles, onStateChange: setState });
return () => {
handles.removeAll();
handles.destroy();
};
}, [basemapToggleVM]);
return {
state,
toggle: () => {
basemapToggleVM?.toggle();
}
};
}
interface HandlerSetup {
basemapToggleVM: __esri.BasemapToggleViewModel;
handles: Handles;
onStateChange: (state: BasemapToggleVM["state"]) => void;
}
function addHandlers({ basemapToggleVM, handles, onStateChange }: HandlerSetup) {
handles.removeAll();
handles.add([
watch(
() => basemapToggleVM.state,
(state: BasemapToggleVM["state"]) => onStateChange?.call(null, state),
{ initial: true }
)
]);
}
Solved! Go to Solution.
Objects in effect dependencies are compared with Object.is(). Without taking a closer look at your example, I’d bet you aren’t memoizing the arguments before they are passed to your hook. Having arguments to a hook at all is kind of a dangerous pattern, you can safeguard in a hacky way by comparing my incoming props to a useReffed value of them with something like fast-deep-equals. If they are comparatively different, then you can mutate your hooks inner state and kick off subsequent effects.
Objects in effect dependencies are compared with Object.is(). Without taking a closer look at your example, I’d bet you aren’t memoizing the arguments before they are passed to your hook. Having arguments to a hook at all is kind of a dangerous pattern, you can safeguard in a hacky way by comparing my incoming props to a useReffed value of them with something like fast-deep-equals. If they are comparatively different, then you can mutate your hooks inner state and kick off subsequent effects.
Thanks Addison - I made a slight change which was to not pass in the props into the new BasemapToggleVm function directly which looked to resolve the issue. Here is a working sample: https://codesandbox.io/p/sandbox/esri-infinite-hook-forked-nrkp39?file=%2Fsrc%2FBasemapToggleButton....