jsapi-resources icon indicating copy to clipboard operation
jsapi-resources copied to clipboard

React Custom UI - Custom filter form with inputs, component state not updating

Open david-sunsyte opened this issue 1 year ago • 7 comments

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

  1. Enter data in the text input in the lower right hand corner of the screen.
  2. 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

david-sunsyte avatar Aug 27 '24 13:08 david-sunsyte

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.

david-sunsyte avatar Aug 27 '24 13:08 david-sunsyte

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;

odoe avatar Aug 27 '24 16:08 odoe

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 avatar Aug 27 '24 16:08 david-sunsyte

@david-sunsyte personally, I would keep it as simple as possible. A button to load your own React component is probably the simplest route.

odoe avatar Aug 29 '24 14:08 odoe

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?

david-sunsyte avatar Aug 29 '24 16:08 david-sunsyte

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);   
 };

david-sunsyte avatar Aug 29 '24 16:08 david-sunsyte

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);
};

david-sunsyte avatar Aug 29 '24 16:08 david-sunsyte

Closing. This issue is more about React than the ArcGIS JS SDK.

andygup avatar Apr 23 '25 21:04 andygup