React Custom UI - Custom filter form with inputs, component state not updating
IMPORTANT
- [X] My question is related to the samples and content within this repository. For all other issues, open a ticket with Esri Technical Support or post your question in the community forum.
- [X] I have checked for existing issues to avoid duplicates. If someone has already opened an issue for what you are experiencing, please add a 👍 reaction and comment as necessary to the existing issue instead of creating a new one. You can also refer to this repo's troubleshooting guide for hints and suggestions.
Actual behavior
I've modified the custom component shown in the custom-ui example (react with vite) as a base. I've added a single text input and a single button input. When I set the state of the text input on change, this works. However, when I click the button input to clear/reset the input state to it's default (""), the custom component seemingly doesn't re-render, and the state of the text input remains.
Expected behavior
I would expect the event handler of the button click to clear/reset the state as defined below, and clear the text input:
import { useState, useEffect } from "react"; import { watch } from "@arcgis/core/core/reactiveUtils"; import "./center-component.css";
/**
* React component that can be used in an ArcGIS JS SDK application.
* This component dynamically displays the center of the map extent
* @param {*} view an instance of a `Mapview` or `SceneView`
* @param {string} id - The `id` of the components's parent HTML div element
* @returns The components's HTML div
*/
const CenterComponent = ({ view, id }) => {
const [center, setCenter] = useState(null);
const [data, setData] = useState("");
useEffect(() => {}, [data]);
useEffect(() => {
// Watch for changes on the View's Extent
let handle = watch(
() => view?.view?.extent,
(value) => {
const { latitude, longitude } = value.center;
// Update the component's display
setCenter(`${longitude.toFixed(4)}, ${latitude.toFixed(4)}`);
}
);
// Clean up any handles or event listeners
// created in useEffect method
return () => handle.remove();
}, [view]);
const handleClearData = () => {
setData("");
};
return (
<div id={id} className="center-component">
Center: {center}
<input
type="text"
placeholder="data"
onChange={(e) => setData(e.target.value)}
/>
<input type="button" onClick={handleClearData} value="Clear data" />
</div>
);
};
export default CenterComponent;
Reproduction sample
https://github.com/david-sunsyte/custom-ui-sample
Reproduction steps
- Enter data in the text input in the lower right hand corner of the screen.
- Click the button labeled 'Clear data'.
Reproduction browser
Version 128.0.6613.36 (Official Build) (64-bit)
Operating System (check https://whatsmyos.com)
Your OS is Windows 10* 64-bit
I should add that the overall problem I'm trying to solve for is to create a custom filter form, in which I can add a series of inputs, for example, Acreage minimum and Acreage maximum, and filter my map view based on the criteria.
So, if you could provide feedback not only on the issue listed above regarding the center component not re-rendering, but also your thoughts on what I'm proposing regarding a filter form, and if you feel there's a better way to achieve this.
This is more of a React usage question, not related the JS SDK, but I can help a bit here.
Your input is not being cleared because the input state is not reflective the useState property you have defined. So it's not going to clear because nothing is hooked up.
If you update your snippet like below, it will work. (you repo link does not work, may be private).
Sample code
import { useState, useEffect } from 'react';
import { watch } from '@arcgis/core/core/reactiveUtils';
import './center-component.css';
const CenterComponent = ({ view, id }) => {
const [center, setCenter] = useState(null);
const [data, setData] = useState("");
useEffect(() => {
console.log("data has changed", data);
}, [data]);
useEffect(() => {
let handle = watch(
() => view?.view?.extent,
(value) => {
const { latitude, longitude } = value.center;
setCenter(`${longitude.toFixed(4)}, ${latitude.toFixed(4)}`);
}
);
return () => handle.remove();
}, [view]);
const handleClearData = () => {
setData("");
};
return (
<div id={id} className="center-component">
Center: {center}
<input
type="text"
placeholder="data"
value={data}
onChange={(e) => setData(e.target.value)}
/>
<input type="button" onClick={handleClearData} value="Clear data" />
</div>
);
};
export default CenterComponent;
Ugghhh, @odoe, I literally totally spaced that....total rookie move/walk of shame for me :( Thank you!! We've actually met and worked together a few times at the dev summit so much appreciated as always!
Regarding the second part of my post, how would you go about creating a 'filter form' in this capacity? I initially set off to have this filter form composed similarly to the CenterComponent example, in which my component would be added to the view ui directly.
I'm now tossing around just adding a custom button to the view ui, which would launch my react component by itself.
Any thoughts/feedback/preference?
@david-sunsyte personally, I would keep it as simple as possible. A button to load your own React component is probably the simplest route.
Will do, thank you! What I'm facing with this path is this...'initialize' below runs twice, which creates two button click listeners, and in fact two view objects.
const filterExpandButton = document.createElement("calcite-button");
view.ui.add(filterExpandButton, "top-right");
export const initialize = async (container, openFilter) => {
view.container = container;
filterExpandButton.addEventListener("click", openFilter);
};
I've seen in one of your other example repos (arcgis-calcite-react) where you seem to account for this by destroying the first view object that get's created when initialize runs the first time.
export async function init(container: HTMLDivElement) {
if (app.view) {
app.view.destroy();
}
const map = new ArcGISMap({
basemap: "topo-vector",
});
const view = new MapView({
map,
container,
center: [-118, 34],
zoom: 8,
});
app.view = view;
return cleanup;
}
How could I got about only creating a single click event listener in the same or similar fashion?
I should add that 'openFilter' is a function that's passed to my map.js from my CustomMapView component.
const openFilter = () => setOpen(!open);
const loadMap = async () => {
const { initialize } = await import("./utils/map");
await initialize(mapDiv.current, openFilter);
};
And although similar to how you approached destroying the first view that gets created, this just feels odd...
export const initialize = async (container, openFilter) => {
view.container = container;
if (filterExpandButton) {
filterExpandButton.removeEventListener("click", openFilter);
}
filterExpandButton.addEventListener("click", openFilter);
};
Closing. This issue is more about React than the ArcGIS JS SDK.