I am using #react-google-maps/api and I'm trying to set click event on gmap features which appends its area to React state.
enter image description here
but every time I click the feature, it seems the state has been set to empty array(initial state). and the event append one string to it. so the state doesn't get longer.
in this component i put one button that fires the same click event and that works fine. I don't know why this happens.
const MapComponent = ({area, setArea}) => {
const { isLoaded, loadError } = useLoadScript({
googleMapsApiKey: "APIKEY"
})
const onClickFeature = e => {
let tgt = e.feature.getProperty("S_NAME")
if(tgt){
addArea(tgt)
}
}
const addArea = tgt => {
let copy = [...area]
copy.push(tgt)
setArea(copy)
}
const renderMap = () => {
const onLoad = async map => {
await getGeoJson().then(res => {
initPolygon(map,res) //draw polygons with feature collection
map.data.addListener("click", e => {
onClickFeature(e)
})
})
}
return <><GoogleMap
mapContainerStyle={containerStyle}
center={center}
zoom={14}
onLoad={onLoad}
onClick={onClickFeature}
>
</GoogleMap>
<button onClick={() => addArea('someplace')}>CLICK</button> {/* This works fine */}
<div>Selected: {area} </div>
</>}
if (loadError) {
return <div>Map cannot be loaded right now, sorry.</div>
}
return isLoaded ? renderMap() : null
}
I can see that your use case should have an area state which should be an array and everytime the geoJsondata feature is clicked, it will get the value of "S_NAME" property. Then it will add it to the area state array.
As you have not provided your whole code, I created your use case using the Google Maps sample geojson data. Everytime the feature is clicked, it will get the "title" property and add it to the "area" state.
Here's the sample code (Note: please use your API key in the index.js for the code to work) and code snippet below:
import React, { useState } from 'react';
import { GoogleMap } from '#react-google-maps/api';
const defaultLocation = { lat: -28, lng: 137 };
function Map() {
const [area, setArea] = useState([]);
const handleOnLoad = map => {
map.data.loadGeoJson(
'https://storage.googleapis.com/mapsdevsite/json/google.json'
);
map.data.addListener('click', onClickFeature);
};
const onClickFeature = event => {
console.log(event.feature.getProperty('letter'));
let letter = event.feature.getProperty('letter');
if (letter) {
addArea(letter);
}
};
const addArea = letter => {
let copy = area;
copy.push(letter);
setArea(copy);
//you will see here that the value of area array is being updated
console.log(area);
};
return (
<GoogleMap
center={defaultLocation}
zoom={5}
onLoad={handleOnLoad}
mapContainerStyle={{ width: '100%', height: '88vh' }}
/>
);
}
export default Map;
Related
I am using react-draft-wysiwyg in my project. It works fine most cases.
Only problem is, when I try to link a text to url. In that case it doesn't update the state.
Let's say, I already have a text "Hello world!". Now if I add more text in this e.g. "Hello world! newText". I want to link this "newText" to url using link option from toolbar and click save. It doesn't update the state with the link and goes back previous text "Hello world!"
Interesting thing is that after adding a link if I add more text to that, it works just fine.
const createStateFromHtml = (html: string) => {
const blocksFromHtml = htmlToDraft(html)
const { contentBlocks, entityMap } = blocksFromHtml
const contentState = ContentState.createFromBlockArray(contentBlocks, entityMap)
return EditorState.createWithContent(contentState)
}
const createHtmlFromState = (editorState: EditorState) => {
const rawContentState = convertToRaw(editorState.getCurrentContent())
return draftToHtml(rawContentState)
}
const htmlSanitizeSettings = {
ADD_TAGS: ['iframe'],
ADD_ATTR: ['allowfullscreen'],
}
const BodyEditor = (props: BodyEditorProps) => {
DOMPurify.setConfig(htmlSanitizeSettings)
const initialState = props.html && props.html !== '' ? createStateFromHtml(props.html) : EditorState.createEmpty()
const [editorState, setEditorState] = React.useState(initialState)
const setEditorHtml = () => {
const html = createHtmlFromState(editorState)
const htmlWithIframeSettings = addPropertiesToIframes(html)
const purifiedHtml = DOMPurify.sanitize(htmlWithIframeSettings)
props.setBody(purifiedHtml)
}
/*
* Adds a div element around iframes and adds class="embedded-video" around so we can use CSS to make iframes reponsive
*/
const addPropertiesToIframes = (html: string) => {
return (
html
// Let's assume embedded iframes are videos
.replace(/<iframe/g, '<div class="iframe-container"><iframe class="embedded-video"')
.replace(/<\/iframe>/g, '</iframe></div>')
.replace(/iframe width=/g, 'iframe allowfullscreen width=')
// Let's remove embedded-video class from embedded spotify iframes
// This should be done for all non-video embeds because otherwise the iframe are made to have 16:9 aspect ratio
.replace(
// eslint-disable-next-line no-useless-escape
/class=\"embedded-video\" src=\"https:\/\/open\.spotify\.com/g,
'src="https://open.spotify.com',
)
)
}
return props.editable ? (
<Editor
editorStyle={editorStyle}
editorState={editorState}
onEditorStateChange={setEditorState}
onBlur={setEditorHtml}
toolbar={{
options: toolbarOptions,
image: {
...imageOptions,
uploadCallback: props.imageUploadCallback,
},
link: {
...linkOptions,
},
embedded: {
...embeddedOptions,
},
}}
placeholder={props.placeholder}
/>
) : (
<div
onClick={() => props.toggleEditable(props.name)}
dangerouslySetInnerHTML={
props.html
? { __html: DOMPurify.sanitize(props.html!) }
: { __html: DOMPurify.sanitize(props.placeholder!) }
}
/>
)
}
export default BodyEditor
Any help would be highly appreciated. I am stuck on this for a very long time.
I used this combine. It is working properly.
import { Editor } from "react-draft-wysiwyg";
import { useEffect } from "react";
import {convertToRaw } from 'draft-js';
import draftToHtml from 'draftjs-to-html';
Maybe, you can try that.
For Example:
You can use to the state initial value below:
EditorState.createWithContent(
ContentState.createFromBlockArray(
convertFromHTML(<your_data>)
)
)
You can use to store the HTML data below:
draftToHtml(convertToRaw(<your_state>.getCurrentContent()))
The Goal:
My React Native App shows a list of <Button /> based on the value from a list of Object someData. Once a user press a <Button />, the App should shows the the text that is associated with this <Button />. I am trying to achieve this using conditional rendering.
The Action:
So first, I use useEffect to load a list of Boolean to showItems. showItems and someData will have the same index so I can easily indicate whether a particular text associated with <Button /> should be displayed on the App using the index.
The Error:
The conditional rendering does not reflect the latest state of showItems.
The Code:
Here is my code example
import {someData} from '../data/data.js';
const App = () => {
const [showItems, setShowItems] = useState([]);
useEffect(() => {
const arr = [];
someData.map(obj => {
arr.push(false);
});
setShowItems(arr);
}, []);
const handlePressed = index => {
showItems[index] = true;
setShowItems(showItems);
//The list is changed.
//but the conditional rendering does not show the latest state
console.log(showItems);
};
return (
<View>
{someData.map((obj, index) => {
return (
<>
<Button
title={obj.title}
onPress={() => {
handlePressed(index);
}}
/>
{showItems[index] && <Text>{obj.item}</Text>}
</>
);
})}
</View>
);
};
This is because react is not identifying that your array has changed. Basically react will assign a reference to the array when you define it. But although you are changing the values inside the array, this reference won't be changed. Because of that component won't be re rendered.
And furthermore, you have to pass the key prop to the mapped button to get the best out of react, without re-rendering the whole button list. I just used trimmed string of your obj.title as the key. If you have any sort of unique id, you can use that in there.
So you have to notify react, that the array has updated.
import { someData } from "../data/data.js";
const App = () => {
const [showItems, setShowItems] = useState([]);
useEffect(() => {
const arr = [];
someData.map((obj) => {
arr.push(false);
});
setShowItems(arr);
}, []);
const handlePressed = (index) => {
setShowItems((prevState) => {
prevState[index] = true;
return [...prevState];
});
};
return (
<View>
{someData.map((obj, index) => {
return (
<>
<Button
key={obj.title.trim()}
title={obj.title}
onPress={() => {
handlePressed(index);
}}
/>
{showItems[index] && <Text>{obj.item}</Text>}
</>
);
})}
</View>
);
};
showItems[index] = true;
setShowItems(showItems);
React is designed with the assumption that state is immutable. When you call setShowItems, react does a === between the old state and the new, and sees that they are the same array. Therefore, it concludes that nothing has changed, and it does not rerender.
Instead of mutating the existing array, you need to make a new array:
const handlePressed = index => {
setShowItems(prev => {
const newState = [...prev];
newState[index] = true;
return newState;
});
}
I am creating a web app, which is basically an image gallery for a browser game.
The avatars are stored in the game in this format:
https://websitelink.com/avatar/1
https://websitelink.com/avatar/2
https://websitelink.com/avatar/3
So i want to build 2 navigation buttons, one will increment the counter, to move to next image and another one will decrement the counter to move to previous image.
I tried to use props, but since props are immutable it didn't work.
How do I approach building this web app?
Here is the minimal code which may help you to understand about the React Component, props and state.
// parent compoment
import { useState } from "react"
export const GameImageGallery = () => {
const [num, setNum] = useState(0)
const increaseDecrease = (state) => {
if (state === "+") {
setNum(num + 1)
}
if (state === "-") {
setNum(num - 1)
}
}
return (
<>
<button onClick={() => increaseDecrease("-")}>--</button>
<button onClick={() => increaseDecrease("+")}>++</button>
<Image url={`https://websitelink.com/avatar/${num}`} />
</>
)
}
// child component to show image
const Image = ({ url }) => {
return <img src={url} alt="image" />
}
you can do this thing,
const [id,setId]=useState(0);
useEffect(() => {
},[id])
const increment = () => {
setId(id++);
}
const decrement = () => {
setId(id--);
}
return(
<button onClick={increment}>Add</button>
<button onClick={decrement}>remove</button>
<img url={`https://websitelink.com/avatar/${id}`} />
)
useRef is ideal to manage data persistently in a component.
Example:
import { useRef } from 'react'
...
const App = () => {
const links = useRef({url1Ctr : 1})
const onBtnClick = () => {
links.current = { url1Ctr: links.current.url1Ctr + 1}
}
...
}
function MyMap({ floorsByBuilding }: MyMapProps) {
const [coords, setCoords] = useState<any[]>([]);
const created = (e) => {
const coordsList = e.layer.editing.latlngs[0][0];
console.log("coordsList:", coordsList);
setCoords(coordsList);
console.log("Coords:",coords);
};
return (
<div className="mapview">
<div className="myMap">
<Map center={[0, 0]} zoom={1}>
<FeatureGroup>
<EditControl
position="topright"
draw={{
rectangle: false,
circle: false,
circlemarker: false,
polyline: false,
}}
onCreated={created}
/>
</FeatureGroup>
</Map>
</div>
</div>
);
}
e.layer.editing.latlngs[0][0] looks like,
[
{
"lat":32.29840589562344,
"lng":80.85780182804785
},
{
"lat":17.421213563227735,
"lng":78.36653079164644
},
{
"lat":23.02755815843566,
"lng":107.33497479055386
},
{
"lat":41.49329414356384,
"lng":104.47883340910323
},
{
"lat":39.47390998063457,
"lng":82.8312041405605
}
]
The EditControl is react-leaflet-draw component that helps to annotate in an image or map, from where we get coordinates (the above data) which is e.layer.editing.latlngs[0][0].
After getting the coordinates, i'm trying to store those coordinates into a state (which is setCoords) so that i can use those coordinates elsewhere.
The issue here is,
after obtaining the coordsList for the first time, the setCoords doesn't actually set those coords ( the second console.log returns empty array, even if the first console.log does return desired output).
But when i'm trying the second time, i.e a new coordList values are obtain, the setCoords sets the data of the pervious coordsList (the first console.log return the coordsList correctly but the second console.log return previous data of coordsList).
Screenshots for more clarity,
First time when I get coordsList,
Second time,
Keep console.log outside the created() function. setCoords() is an async function, as a result state won't be updated right away to show it in the console.log as you have given. So keep it outside to view it when re-rendering.
const created = (e) => {
const coordsList = e.layer.editing.latlngs[0][0];
console.log("coordsList:", coordsList);
setCoords(coordsList);
};
console.log("Coords:",coords);
Because state only has new value when component re render. So you should move console.log to useEffect to check the new value when component re-render.
const created = (e) => {
const coordsList = e.layer.editing.latlngs[0][0];
console.log("coordsList:", coordsList);
setCoords(coordsList);
};
useEffect(() => {
console.log("Coords:", coords);
}, [coords]);
It's because Reactjs's update batching mechanism
When updating state within an eventHandler like onChange, onClick,.etc.., React will batch all of setState into one:
const created = (e) => {
const coordsList = e.layer.editing.latlngs[0][0];
console.log("coordsList:", coordsList);
setCoords(coordsList);
console.log("Coords:", coords);
};
Place your log right before return function to see that lastest value:
function MyMap({ floorsByBuilding }: MyMapProps) {
const [coords, setCoords] = useState<any[]>([]);
const created = (e) => {
const coordsList = e.layer.editing.latlngs[0][0];
setCoords(coordsList);
};
console.log("Coords:",coords);
return (
...
)
}
But this behaviour will be changed in Reactjs 18 as mentioned here
The code below is my minimal issue reproduce component. It initializes fabric canvas, and handles "mode" state. Mode state determines whether canvas can be edited and a simple button controls that state.
The problem is that even if mode,setMode works correctly (meaning - components profiler shows correct state after button click, also text inside button shows correct state), the state returned from mode hook inside fabric event callback, still returns initial state.
I suppose that the problem is because of the function passed as callback to fabric event. It seems like the callback is "cached" somehow, so that inside that callback, all the states have initial values, or values that were in state before passing that callback.
How to make this work properly? I would like to have access to proper, current state inside fabric callback.
const [canvas, setCanvas] = useState<fabric.Canvas | null>(null);
const [mode, setMode] = useState("freerun");
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const modes = ["freerun", "edit"];
React.useEffect(() => {
const canvas = new fabric.Canvas(canvasRef.current, {
height: 800,
width: 800,
backgroundColor: 'yellow'
});
canvas.on('mouse:down', function (this: typeof canvas, opt: fabric.IEvent) {
const evt = opt.e as any;
console.log("currentMode", mode) // Not UPDATING - even though components profiler shows that "mode" state is now "edit", it still returns initial state - "freerun".
if (mode === "edit") {
console.log("edit mode, allow to scroll, etc...");
}
});
setCanvas(canvas);
return () => canvas.dispose();
}, [canvasRef])
const setNextMode = () => {
const index = modes.findIndex(elem => elem === mode);
const nextIndex = index + 1;
if (nextIndex >= modes.length) {
setMode(modes[0])
} else {
setMode(modes[nextIndex]);
}
}
return (
<>
<div>
<button onClick={setNextMode}>Current mode: { mode }</button>
</div>
{`Current width: ${width}`}
<div id="fabric-canvas-wrapper">
<canvas ref={canvasRef} />
</div>
</>
)
The problem is that mode is read and it's value saved inside the callback during the callback's creation and, from there, never update again.
In order to solve this you have to add mode on the useEffect dependencies. In this way each time that mode changes React will run again the useEffect and the callback will receive the updated (and correct) value.
That's true! It worked now, thanks Marco.
Now to not run setCanvas on each mode change, I ended up creating another useEffect hook to hold attaching canvas events only. The final code looks similar to this:
const [canvas, setCanvas] = useState<fabric.Canvas | null>(null);
const [mode, setMode] = useState("freerun");
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const modes = ["freerun", "edit"];
React.useEffect(() => {
if (!canvas) {
return;
}
// hook for attaching canvas events
fabric.Image.fromURL(gd, (img) => {
if (canvas) {
canvas.add(img)
disableImageEdition(img);
}
});
canvas.on('mouse:down', function (this: typeof canvas, opt: fabric.IEvent) {
const evt = opt.e as any;
console.log("currentMode", mode) // works correctly now
if (mode === "edit") {
console.log("edit mode, allow to scroll, etc...");
}
});
}, [canvas, mode])
React.useEffect(() => {
const canvas = new fabric.Canvas(canvasRef.current, {
height: 800,
width: 800,
backgroundColor: 'yellow'
});
setCanvas(canvas);
return () => canvas.dispose();
}, [canvasRef])
const setNextMode = () => {
const index = modes.findIndex(elem => elem === mode);
const nextIndex = index + 1;
if (nextIndex >= modes.length) {
setMode(modes[0])
} else {
setMode(modes[nextIndex]);
}
}
return (
<>
<div>
<button onClick={setNextMode}>Current mode: { mode }</button>
</div>
{`Current width: ${width}`}
<div id="fabric-canvas-wrapper">
<canvas ref={canvasRef} />
</div>
</>
)
I also wonder if there are more ways to solve that - is it possible to solve this using useCallback hook?