Open Layer Renders Map component twice in react - reactjs

I'm using Open Layer to render map functionality in a react application. I used the simple code snippets to display the map in the application. Unfortunately the application renders the map twice as below.
Here is the code snippet I used to display the map:
export default function MapView() {
const view = new View({
center: [0, 0],
zoom: 2,
});
useEffect(() => {
new Map({
layers: [
new TileLayer({source: new OSM()}),
],
view: view,
target:'map',
})
},[])
return (
<>
<div id="map" style={{width:'100%',height:'400px'}}></div>
</>
)
}
And the parent component is as below
function App() {
return (
<div>
<MapView />
</div>
);
}

Your component may be mounted multiple times; useEffect with an empty deps array runs each time the component is mounted, but instantiating View/Map appears to mutate the DOM directly and inject the map. If you look at my example, the message "I'm mounting!" is printed to the console twice. Because useEffect is running once per mount, you're instantiating multiple Map objects and attaching them to the element with id map. Instead, you should consider using a ref to hold your Map reference so that only one map object is instantiated per instance of your component.
Additionally, rather than referencing an ID by string in your map mounting, you should instead use a ref for the component's rendered node, instead, and pass that reference to target (which does appear to accept an HTMLElement node directly).
See this sandbox link for an example.
This ref handle also gets you a persistent reference to the map object, which you can then use to perform imperative operations on the map in response to prop changes (see my example zoom-sensitive useEffect).

Related

How to access arcgis map via components?

The code below is taken directly from arcgis via react on how to display a map.
If i wanted to say, zoom in to a set of coordinates, but the code for that was set in another component, how can i get that component to talk to the map here in this component?
import Map from '#arcgis/core/Map';
import MapView from '#arcgis/core/views/MapView';
const map = new Map({
basemap: "topo-vector"
});
const view = new MapView({
container: "viewDiv",
map: map
});
I resolved this by using redux toolkit to set the map as a global state object.
The entire map view is set in a useEffect, once i initialize each of the views, i dispatch the map view to a reducer in rtk.
dispatch(updateMapViewState(view));
updateMapViewState: (state, action) => {
state.view = action.payload
},
Then, when i want to use the map in a separate component, i do:
const view = useSelector((state) => state.MapSlice.view);
In this way, all components can access the map outside of the useEffect in the map component, and can manipulate it without creating a new map view. This worked for me. I assume you could probably do this with context api, but we aren't using that as a global state manager.
This may not be the recommended or best way to achieve this, but I had success by passing the view object from the map component back to the parent component, then saving it to the parent component's state.
// in App.js
saveViewToState(view){
this.setState({view: view})
}
<MapComponent saveViewToState={this.saveViewToState}/>
// in MapComponent.js
let view = new View()
this.props.saveViewToState(view)
Then I was able to interact with the view object from the parent:
// in App.js
this.state.view.extent = {xmin: 1, ymin: 1, xmax: 2, ymax: 2}
This doesn't work perfectly (for some reason I can't call view.goTo, but view.extent works). I'd be keen to hear if there is a better way to achieve this.
Still looking for a clear answer to this problem.
Have not found an example separating the map/view creation and adding layers into differing components.

React App: how to pass a variable to other file and read its value there

I am building on sample 16 from Github/Webchat to build a webpage with a webchat interface.
https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/16.customization-selectable-activity
The react app consists off different .js files to build the website( Webchat.js, Instuctor.js, App.js, Index.js) and I can't provide file Inspector.js with the data I gathered in file Webchat.js.
I can't find the right code to read the value of a variable from file Webchat.js in file Inspector.js.
I want to build a Webpage where I have on the left side a Chatbot (BotFrameWork) running, and next to it a table running which shows data that has been collected by the chatbot.
I tried answers from
how to get a variable from a file to another file in node.js
but doesn't work.
I tried to get the state of Webchat but gives only undefined.
Example:
(webchat.js) I fetched data from the bot (like [link]How to create a side window or iframe that shows data that was gathered by a Web-Chat in one web page?) and saved it in a state variable 'test'.
(instructor.js) I want to show that data e.g. in a label that is getting updated when new data comes in. How do I get access now to the value of 'test' that is created in another file?
What doesnt work:
in instuctor.js:
var test2 = require ('./webchat');
Console.log(test2.state.test) //this is how I imagine it to work --> undefined
with require I only get an object that has a 'name' variable 'Webchat' and which i can get out with: console.log(test2.default.name);
React only supports one-way data binding, so if you want to share a variable between multiple components, you need to elevate the state to the parent and pass the variable and change handlers to the children as props.
In the example below, Parent has two children: ChildA and ChildB. We could keep myValue in ChildA's state, but then ChildB wouldn't be able to access it, so we elevate myValue to the parent and pass it to both children as props. We also, pass a change handler to ChildB so it can update the value when the user clicks it.
import React from 'react';
const ChildA = ({ myValue }) => (
<div>
{
myValue
? <h1>Hello World</h1>
: <h1>Goodbye!</h1>
}
</div>
);
const ChildB = ({ myValue, handleMyValueChange}) => (
<button onClick={ () => handleMyValueChange(false) } disabled={ myValue }>
Click Me!
</button>
);
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = { myValue: true }
}
render() {
return (
<div>
<ChildA myValue={this.props.myValue}/>
<ChildB myValue={this.props.myValue} handleMyValueChange={ handleMyValueChange }/>
</div>
)
}
handleMyValueChange = myValue => {
this.setState({ myValue });
}
}
In terms of the sample you referenced, the parent class is App and the two children are ReactWebChat and Inspector. I would recommend elevating the state of your variable to the parent - App - and pass it as a prop to the Inspector class. Then you can add a custom store middleware to ReactWebChat that updates your variable when the bot sends an update event. For more information on how to configure your bot to send update events and how to make Web Chat listen for them, take a look at this StackOverflow Question.
Hope this helps!

Display a Spinner while loading Parent State Array

I am using React with typescript for my application. What I have now is in my top level “app.tsx” a state called objects which is an array of objects. I have passed down through a few child components a function that is getObjects which is defined in the top app level. This function returns the state object[] if it is defined otherwise calls an asynchronous function to load in the objects.
What I would like to do is to display a loading spinner in my child component while this state object[] is still undefined in the app.
I know that if I were to load the object[] as a local state in my child component I could easily accomplish this. Is there any way that I can call a callback at the child level after the state in the app has finished loading? The only way that I see to do it is to pass the object[] itself to the child component, but my knowledge is still very limited. Thank you, I appreciate all help.
Edit:
I do need the getObjects function in my grandchild component. I've simplified the structure a bit. What really is the situation is in the app object is a Dictionary type that acts as a key-value pair with a number id corresponding to a specific array of objects. On app loading, one key-value pair is chosen and that is loaded asynchronously while the user can keep using the app. It loads in the backend for these initial child components so a spinner is not needed.
Later in the greatgrandchild component, there is a need to, if chosen by the user, load another id into the dictionary while using an API to populate the object[] portion of the dictionary. Once this portion of the dictionary is loaded, the getObjects call in the top app layer returns a converted array of objects in my greatgrandchild to be used for selection. What is happening now is that the selectbox is just being empty until the async function completes, then it populates as expected. What I would like to do is to display a spinner in the greatgrandchild in place of the selectbox while this operation completes.
Since the structure is a dictionary and technically two values for the dictionary could be trying to be loaded at a time, I'm not sure that I want to try to pass through and use a boolean for this loading.
I am reluctant to pass through the whole dictionary itself and wait for the particular dictionary key-value pair to be loaded since this seems like an anti-pattern. What is the best way to accomplish what I am trying to do?
In your child component conditionally render the spinner and the object contents.
{
yourObjectFromParent
? (<>Your object contents here</>)
: (<LoaderComponent/>)
}
OR
You can try to set a state called loading=true in the parent and set it to false when object is loaded. You should pass this loading state as props to the child components. If thats the case your code will be like this. This can also be implemented with a global state handler like redux.
{
props.loading
? (<>Your object contents here</>)
: (<LoaderComponent/>)
}
This would show a spinner until loading is completed.
class MyComponent extends Component {
state = {
isLoading: true,
data: null
}
componentDidMount() {
fetchSomeData(url)
.then(data => {
this.setState({isLoading: false, data: data});
})
}
render() {
const {isLoading, data} = this.state;
return isLoading ? <ChildComponent data={data} /> : <Spinner />
}
}

Fit map to feature layer bounds in react-leaflet

What I want to achieve:
Have a <Map><FeatureGroup><Circle />[1 or more]...</FeatureGroup></Map> hierarchy and fit the map bounds to the feature group so that all the circles are in the viewport.
If there is only one circle, it should fit the bounds (ie: zoom in on) to that circle.
What I've tried:
giving FeatureGroup a ref and calling getBounds on it to pass onto Map. Because of the lifecycle FeatureGroup doesn't exist at the time componentDidMount is called - it gets rendered later (https://github.com/PaulLeCam/react-leaflet/issues/106#issuecomment-161594328).
Storing Circle in state and calling getBounds on that (assuming, in this case, that there is only one circle. That didn't work either.
I think I might need to do something with the React Context but I'm not sure that I fully understand it right now, so I need some help.
Other information
I'm using react-leaflet#2.1.2
Thanks for any help offered!
Because the contents of the Map are unavailable at componentDidMount-time (https://github.com/PaulLeCam/react-leaflet/issues/106#issuecomment-161594328) you cannot get the bounds of the FeatureGroup at that point, and out of all the refs you assign, only the Map ref will be available in this.refs.
However, as per this GitHub comment: https://github.com/PaulLeCam/react-leaflet/issues/106#issuecomment-366263225 you can give a FeatureGroup an onAdd handler function:
<FeatureGroup ref="features" onAdd={this.onFeatureGroupAdd}>...
and you can then use the Map refs to access the leafletElement and call fitBounds with the bounds of the incoming event target, which will be the FeatureGroup:
onFeatureGroupAdd = (e) => {
this.refs.map.leafletElement.fitBounds(e.target.getBounds());
}
This will then "zoom" the map into the bounds of your FeatureGroup, as desired.
Update
I modified my React component so that zoom and centre are controlled by query parameters. The problem with the above solution was that if you zoomed in on a MarkerClusterGroup by clicking on it, for example, it would update the zoom in the url, re-render the map and re-call onFeatureGroupAdd, thus undoing all the marker cluster goodness.
What I needed was to access the zoom level required to keep the newly drawn circle nicely in bounds, then update the url with the correct zoom level and center.
onDrawCircle = (e) => {
...
var targetZoom = this.refs.map.leafletElement.getBoundsZoom(e.layer.getBounds());
// Call function to update url here:
functionToUpdateUrl(targetZoom, e.layer.getBounds().getCenter());
}
}
In order to be able to control the whole map I also call functionToUpdateUrl in onZoomEnd and onDragEnd event handlers, like so:
onChangeView = (e) => {
functionToUpdateUrl(e.target._zoom, this.refs.map.leafletElement.getCenter());
}
and one for handling cluster clicks:
onClusterClick = (e) => {
// This time we want the center of the layer, not the map?
functionToUpdateUrl(e.target._zoom, (e.layer ? e.layer.getBounds().getCenter() : e.target.getBounds().getCenter()));
}
Then, when rendering the Map element, pass these properties:
<Map
center={center}
ref='map'
zoom={zoom}
maxZoom={18}
onZoomEnd={this.onChangeView}
onDragEnd={this.onChangeView}
>
....
</Map>
And remember to give any MarkerClusterGroups their onClusterClick callback:
<MarkerClusterGroup onAdd={this.onMarkerGroupAdd} onClusterClick={this.onClusterClick}>
Have you tried doing getBounds in the componentDidMount function instead of componentWillMount? If that doesn't work then I'd suggest extending the FeatureGroup component and adding an onLoaded function as as prop and call that function in the componentDidMount function of your extended component. And by extending the FeatureGroup component I actually mean copying/pasting it from here. (if you care about why you need to copy that whole file check this thread)
This isn't tested but your code will probably look something like
import { FeatureGroup } from 'leaflet';
import { withLeaflet, Path } from 'react-leaflet';
class CustomFeatureGroup extends Path {
createLeafletElement(prop) {
const el = new FeatureGroup(this.getOptions(props));
this.contextValue = {
...props.leaflet,
layerContainer: el,
popupContainer: el,
};
return el;
}
componentDidMount() {
super.componentDidMount();
this.setStyle(this.props);
/*
Here you can do your centering logic with an onLoad callback or just
by using this.leafletElement.map or whatever
*/
this.props.onLoaded();
}
}
export default withLeaflet(CustomFeatureGroup)
Note: If you are using react-leaflet V1 this is actually way easier and I can edit this answer with that code if needed.

Although React doesn't allow objects as children, my code clearly says otherwise

So I came across this bizarre behavior — when I try to setState using objects, it only fails in SOME cases.
For example this works,
getUserClaimAmount().then(
userClaimAmount => {
this.setState({ userClaimAmount: userClaimAmount.toString()})
}
);
But the following does not. It will throw an error saying that React children are not allowed to be objects.
getUserClaimAmount().then(
userClaimAmount => {
this.setState({ userClaimAmount: userClaimAmount})
}
);
However, the following works for some reason. "bettingPoolTotal" is the same type as "userClaimAmount" above.
getBettingPoolTotal().then(
bettingPoolTotal => {
this.setState({ total: bettingPoolTotal });
}
);
Below is a screenshot of what the state looks like. It's clear that there's obviously React children that are indeed objects.
Example of React state
You've mixed up the children property and the state of the component.
children property is populated by React when you create the component. Considering the following JSX:
<Parent>
<Child1 />
text here
<Child2 />
</Parent>
The children property of Parent will be an array with three entries: the element created from Child1, string "text here" and the element created from Child2. If you try to pass the plain JavaScript object into the final markup (not into the React component, since it can use it as it want without error, but into native HTML tag), it won't work:
<div>
{{key: value}}
</div>
That's exactly the error you're getting. It seems that there is some element which has this.state.userClaimAmount passed as children entity, and so errors when it gets a plain object instead of string or JSX.
As for the state - it's entirely possible to have plain objects inside it; however, this should be used very cautiously. Consider the following case inside some component which has an obj field in its state:
const obj = this.state.obj;
obj.key = 'value';
this.setState({obj: obj});
The last setState here surprisingly become a no-op, since the state is already changed by this moment - through the reference copied to obj, - and then React, knowing that this is a no-op, doesn't trigger rendering, leaving component the same as before.

Resources