We are building a React-Redux web app that will display multiple Three JS scenes. These scenes come in pairs, and each pair will have synchronized zooming. To facilitate that, we're storing camera data in the Redux store.
Here is our React class (take a deep breath, it's a little long for a SO question), which uses react-three-renderer to produce Three JS objects:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Vector3 } from 'three';
import React3 from 'react-three-renderer';
import ReferenceGrid from './ReferenceGridVisual';
import ResourceGroup from './resourceGroups/ResourceGroup';
import { initializeRayCastScene } from './viewportMath/RayCastScene';
import zoomCamera from './viewportMath/CameraZoom';
import { registerCamera, zoom } from './actions/cameraActions';
import { InitThreeJsDomEvents, UpdateDomCamera } from './domUtility/ThreeJSDom';
class ThreeJsScene extends Component {
constructor(props) {
super(props);
this.ZoomAmount = 150;
this.ZoomMaxCap = 1000;
this.ZoomMinCap = 6;
this.zoomPadding = 10;
this.minimumZoom = 45;
}
componentWillMount() {
initializeRayCastScene();
this.props.registerCamera(this.props.sceneName);
}
componentDidMount() {
// eslint-disable-next-line no-underscore-dangle
InitThreeJsDomEvents(this.camera, this.canvas._canvas);
}
onWheel = (event) => {
// eslint-disable-next-line
this.zoom(event.clientX, event.clientY, event.deltaY);
}
setCameraRef = (camera) => {
UpdateDomCamera(camera);
this.camera = camera;
}
zoom(screenPosX, screenPosY, zoomAmount) {
const size = {
width: this.props.width,
height: this.props.height,
};
const result = zoomCamera(screenPosX, screenPosY, zoomAmount, this.camera.position,
size, this.props.distance, this.camera, this.props.cameraType, this.ZoomMaxCap,
this.ZoomMinCap);
this.ZoomAmount = (result.ZoomAmount) ? result.ZoomAmount : this.ZoomAmount;
this.props.zoom(this.props.sceneName, result.distanceChangeFactor, result.newCameraPosition);
}
render() {
let position;
if (this.props.cameraPosition != null) {
position = new Vector3(
this.props.cameraPosition.x,
this.props.cameraPosition.y,
this.props.cameraPosition.z
);
} else {
position = new Vector3();
}
const left = -this.props.width / 2;
const right = this.props.width / 2;
const top = this.props.height / 2;
const bottom = -this.props.height / 2;
return (
<div
style={{ lineHeight: '0' }}
onWheel={this.onWheel}
>
<React3
width={this.props.width}
height={this.props.height}
mainCamera="camera"
antialias
pixelRatio={1}
ref={(canvas) => { this.canvas = canvas; }}
>
<scene ref={(scene) => { this.scene = scene; }}>
<orthographicCamera
name="camera"
left={left}
right={right}
top={top}
bottom={bottom}
near={0.01}
far={1400}
position={position}
ref={this.setCameraRef}
/>
<ambientLight
color={0xaaaaaa}
/>
<directionalLight
color={0xaaaaaa}
intensity={1.1}
position={new Vector3(3, 4, 10)}
lookAt={new Vector3(0, 0, 0)}
/>
<ReferenceGrid xActive yActive zActive={false} store={this.props.store} />
<ResourceGroup store={this.props.store}>
{this.props.children}
</ResourceGroup>
</scene>
</React3>
</div>
);
}
}
const mapStateToProps = (state, ownProps) => {
const ownCamera = state.cameras.get(ownProps.sceneName);
if (ownCamera == null) {
console.log('own camera null');
return { cameraAvailable: false };
}
console.log('has own camera');
const cameraPosition = ownCamera.position;
const cameraType = ownCamera.type;
const distance = ownCamera.distance;
return {
cameraAvailable: true,
cameraPosition,
cameraType,
distance,
};
};
const mapDispatchToProps = dispatch => ({
registerCamera: (cameraName) => {
dispatch(registerCamera(cameraName));
},
zoom: (cameraName, factor, newCameraPosition) => {
dispatch(zoom(cameraName, factor, newCameraPosition));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ThreeJsScene);
Additionally, for reference, here are the action creators:
export const registerCamera = cameraName => (dispatch) => {
dispatch({ type: 'REGISTER_CAMERA', newCameraName: cameraName });
};
export const zoom = (cameraName, factor, newCameraPosition) => (dispatch, getState) => {
const state = getState();
const zoomFactor = state.cameras.get(cameraName).distance * (1 - factor);
dispatch({ type: 'CAMERA_ZOOM', cameraName, factor: zoomFactor, newCameraPosition });
};
And the reducer:
import { Map } from 'immutable';
const defaultCameraProperties = {
distance: 150,
type: 'orthogonal',
position: { x: 0, y: 10, z: 50 },
rotation: { x: 0, y: 0, z: 0, w: 1 },
};
const initialState = Map();
export default (state = initialState, action) => {
switch (action.type) {
case 'REGISTER_CAMERA': {
const newCamera = {
...defaultCameraProperties,
...action.newCameraProperties,
};
return state.set(action.newCameraName, newCamera);
}
case 'CAMERA_ZOOM': {
const updatedDistance = action.factor;
const updatedCameraPosition = {
...state.get(action.cameraName).position,
...action.newCameraPosition,
};
const updatedCamera = {
...state.get(action.cameraName),
position: updatedCameraPosition,
distance: updatedDistance,
};
return state.set(action.cameraName, updatedCamera);
}
default: {
return state;
}
}
};
The challenge is in the zoom function in the React class, the React props are not what I would expect, and therefore zooming is failing. Here is a summary of the sequence of relevant events as I understand them:
componentWillMount is called, which dispatches the REGISTER_CAMERA method. (We do this rather than having camera data by default in the store because these pairs of scenes are generated dynamically - there is not a static number of them.)
The React render method is called.
The React render method is called again since the REGISTER_CAMERA action has now modified the store and we have new props - the camera related props are now available.
I trigger zoom with my mouse wheel. The onWheel handler calls the zoom function, but breakpointing in that method reveals that the camera related props - like this.props.cameraType - are undefined. The React props appear as they do in 2. (zoomCamera does some calculations. Since these properties are unavailable, zooming fails.)
I can't figure out why this is. My suspicion is I'm misunderstanding something about what this context is bound to the zoom method.
In short my question is why are my props not up to date and how can I make the updated version available to the zoom function?
Turns out it was an error with hot module reloading. Running our build cold does not exhibit the issue.
Related
I'm building an application using react-leaflet and I recently updated all of my dependencies which brought on an error that I can't solve.
The error:
React has detected a change in the order of Hooks called by ForwardRef(ContainerComponent)
Removing the <MapContainer> component from my app fixes the error, but I cannot seem to figure out where the ForwardRef component is being rendered or why the order of hooks changes within it between renders.
This is my component:
const Map = ({ openModal }) => {
const [homeCoords, setHomeCoords] = useState([49.2, -123]);
const [bounds, setBounds] = useState({
lat_lower: 48.9,
lat_upper: 49.5,
lon_left: -123.8,
lon_right: -122.2
});
// Get the user's location with navigator.geolocation
useEffect(() => {
if(!window.navigator.geolocation) {
return;
}
window.navigator.geolocation.getCurrentPosition(
// success
(res) => {
setHomeCoords([res.coords.latitude, res.coords.longitude]);
},
// failure
() => {
console.error('Must allow pestlocations.com to access your location to use this feature.')
}
)
}, []);
// Helper function for BoundTracker component
const collectBounds = (e) => {
const bounds = e.target.getBounds();
const currLatLower = bounds._southWest.lat;
const currLatUpper = bounds._northEast.lat;
const currLonLeft = bounds._southWest.lng;
const currLonRight = bounds._northEast.lng;
setBounds({
lat_lower: currLatLower,
lat_upper: currLatUpper,
lon_left: currLonLeft,
lon_right: currLonRight
})
}
// Listen for dragging or zooming on map and update bounds
const BoundTracker = () => {
useMapEvents({
// Drag map
dragend: (e) => {
collectBounds(e);
},
// Zoom map
zoomend: (e) => {
collectBounds(e);
}
})
}
const HomeButton = () => {
const map = useMap();
return (
<div className="btn home" aria-disabled="false" onClick={() => {
map.panTo(homeCoords);
const bounds = map.getBounds();
setBounds({
lat_lower: bounds._southWest.lat,
lat_upper: bounds._northEast.lat,
lon_left: bounds._southWest.lng,
lon_right: bounds._northEast.lng
})
}}>
<AiFillHome />
</div>
)
}
return (
<>
<MapContainer className="Map" position={homeCoords} zoom={10}>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<HomeButton />
<div className="btn info" onClick={() => openModal("Welcome")}><AiOutlineInfoCircle /></div>
<div className="btn legend" onClick={() => openModal("Legend")}><BsMap /></div>
<div className="search-btn" onClick={() => return}>Search This Area</div>
<PointClusters />
<BoundTracker />
</MapContainer>
</>
)
}
EDIT
Here is my PointClusters component:
import { useState } from 'react';
import MarkerClusterGroup from 'react-leaflet-cluster';
import { Marker, Popup } from 'react-leaflet';
import L from 'leaflet';
import './PointClusters.css';
// For testing purposes only - - - - - - - - - - - - - - - - - -
const testPoints = require('../../../testPoints.json').features;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const PointClusters = () => {
// Initialize points as testpoints. ** CHANGE FOR PRODUCTION **
const [points, setPoints] = useState(testPoints);
const newicon = new L.divIcon({
className: "custom-marker",
html: "<span class='arrow'></span>"
});
const createClusterCustomIcon = function (cluster) {
return L.divIcon({
html: `<span>${cluster.getChildCount()}</span>`,
className: 'custom-marker-cluster',
iconSize: L.point(33, 33, true),
})
}
return (
<MarkerClusterGroup
iconCreateFunction={createClusterCustomIcon}
>
{
points.map((point, i) => {
const { name, address, type, numOfReports, url } = point.properties;
const coords = [point.geometry.coordinates[1], point.geometry.coordinates[0]];
return (
<Marker position={coords} icon={newicon} key={i}>
<Popup>
{
name === "None" ? null :
<>
<b>{name}</b>
<br />
</>
}
<strong>Address</strong>: {address}
<br />
<strong>Type</strong>: {type}
<hr />
At least <a href={url}>{numOfReports} reports</a> on bedbugregistry.com
</Popup>
</Marker>
)
})
}
</MarkerClusterGroup>
)
}
Since you only have the useState hook in PointClusters I assume the issue here is react-leaflet-cluster package. I know it did not have support för react-leaflet 4 when I wanted to use it. It now have a version 2.0.0 that should be compatible, however looking into the code they use hooks in the solution.
Since they did not support react-leaflet 4 when I needed it I decided to adapt the actual code and modify to work and to fit my needs. Below is that adaption:
import { createPathComponent } from "#react-leaflet/core";
import L, { LeafletMouseEventHandlerFn } from "leaflet";
import "leaflet.markercluster";
import { ReactElement, useMemo } from "react";
import { Building, BuildingStore, Circle } from "tabler-icons-react";
import { createLeafletIcon } from "./utils";
import styles from "./LeafletMarkerCluster.module.css";
import "leaflet.markercluster/dist/MarkerCluster.css";
type ClusterType = { [key in string]: any };
type ClusterEvents = {
onClick?: LeafletMouseEventHandlerFn;
onDblClick?: LeafletMouseEventHandlerFn;
onMouseDown?: LeafletMouseEventHandlerFn;
onMouseUp?: LeafletMouseEventHandlerFn;
onMouseOver?: LeafletMouseEventHandlerFn;
onMouseOut?: LeafletMouseEventHandlerFn;
onContextMenu?: LeafletMouseEventHandlerFn;
};
// Leaflet is badly typed, if more props needed add them to the interface.
// Look in this file to see what is available.
// node_modules/#types/leaflet.markercluster/index.d.ts
// MarkerClusterGroupOptions
export interface LeafletMarkerClusterProps {
spiderfyOnMaxZoom?: boolean;
children: React.ReactNode;
size?: number;
icon?: ReactElement;
}
const createMarkerCluster = (
{
children: _c,
size = 30,
icon = <Circle size={size} />,
...props
}: LeafletMarkerClusterProps,
context: any
) => {
const markerIcons = {
default: <Circle size={size} />,
property: <Building size={size} />,
business: <BuildingStore size={size} />,
} as { [key in string]: ReactElement };
const clusterProps: ClusterType = {
iconCreateFunction: (cluster: any) => {
const markers = cluster.getAllChildMarkers();
const types = markers.reduce(
(
acc: { [x: string]: number },
marker: {
key: string;
options: { icon: { options: { className: string } } };
}
) => {
const key = marker?.key || "";
const type =
marker.options.icon.options.className || key.split("-")[0];
const increment = (key.split("-")[1] as unknown as number) || 1;
if (type in markerIcons) {
return { ...acc, [type]: (acc[type] || 0) + increment };
}
return { ...acc, default: (acc.default || 0) + increment };
},
{}
) as { [key in string]: number };
const typeIcons = Object.entries(types).map(([type, count], index) => {
if (count > 0) {
const typeIcon = markerIcons[type];
return (
<div key={`${type}-${count}`} style={{ display: "flex" }}>
<span>{typeIcon}</span>
<span style={{ width: "max-content" }}>{count}</span>
</div>
);
}
});
const iconWidth = typeIcons.length * size;
return createLeafletIcon(
<div style={{ display: "flex" }} className={"cluster-marker"}>
{typeIcons}
</div>,
iconWidth,
undefined,
iconWidth,
30
);
},
showCoverageOnHover: false,
animate: true,
animateAddingMarkers: false,
removeOutsideVisibleBounds: false,
};
const clusterEvents: ClusterType = {};
// Splitting props and events to different objects
Object.entries(props).forEach(([propName, prop]) =>
propName.startsWith("on")
? (clusterEvents[propName] = prop)
: (clusterProps[propName] = prop)
);
const instance = new (L as any).MarkerClusterGroup(clusterProps);
instance.on("spiderfied", (e: any) => {
e.cluster._icon?.classList.add(styles.spiderfied);
});
instance.on("unspiderfied", (e: any) => {
e.cluster._icon?.classList.remove(styles.spiderfied);
});
// This is not used at the moment, but could be used to add events to the cluster.
// Initializing event listeners
Object.entries(clusterEvents).forEach(([eventAsProp, callback]) => {
const clusterEvent = `cluster${eventAsProp.substring(2).toLowerCase()}`;
instance.on(clusterEvent, callback);
});
return {
instance,
context: {
...context,
layerContainer: instance,
},
};
};
const updateMarkerCluster = (instance: any, props: any, prevProps: any) => {};
const LeafletMarkerCluster = createPathComponent(
createMarkerCluster,
updateMarkerCluster
);
const LeafletMarkerClusterWrapper: React.FC<LeafletMarkerClusterProps> = ({
children,
...props
}) => {
const markerCluster = useMemo(() => {
return <LeafletMarkerCluster>{children}</LeafletMarkerCluster>;
}, [children]);
return <>{markerCluster}</>;
};
export default LeafletMarkerClusterWrapper;
I combine different types of markers and show each icon with the number in the cluster. You should be able to replace iconCreateFunction to fit your needs.
The createLeafletIcon look like this:
import { divIcon } from "leaflet";
import { ReactElement } from "react";
import { renderToString } from "react-dom/server";
export const createLeafletIcon = (
icon: ReactElement,
size: number,
className?: string,
width: number = size,
height: number = size
) => {
return divIcon({
html: renderToString(icon),
iconSize: [width, height],
iconAnchor: [width / 2, height],
popupAnchor: [0, -height],
className: className ? className : "",
});
};
Another tip is to looking into using useMemo on props, that the system might otherwise see as "new", but the first order of action should make it work, then you can try to find props that cause rerenders. Best of luck and let me know if you have any questions regarding the implementation
I have a question of something that looks pretty obvious but It's getting hard for me. I know that for fetching data that will get actually rendered in a component you need to use reacthooks and useState. However I am having a problem because I need to fetch some data and then store it in a variable that it's not part of the component rendering. This is my current code.
import React from 'react'
import { GoogleMap, useJsApiLoader } from '#react-google-maps/api';
import { GoogleMapsOverlay } from "#deck.gl/google-maps";
import {GeoJsonLayer, ArcLayer} from '#deck.gl/layers';
import axios from 'axios';
import {useState} from 'react';
const hasWindow = typeof window !== 'undefined';
function getWindowDimensions() {
const width = hasWindow ? window.innerWidth : null;
const height = hasWindow ? window.innerHeight : null;
return {
width,
height,
};
}
const center = {
lat: 51.509865,
lng: -0.118092
};
const deckOverlay = new GoogleMapsOverlay({
layers: [
new GeoJsonLayer({
id: "airports",
data: markers,
filled: true,
pointRadiusMinPixels: 2,
opacity: 1,
pointRadiusScale: 2000,
getRadius: f => 11 - f.properties.scalerank,
getFillColor: [200, 0, 80, 180],
pickable: true,
autoHighlight: true
}),
new ArcLayer({
id: "arcs",
data: markers,
dataTransform: d => d.features.filter(f => f.properties.scalerank < 4),
getSourcePosition: f => [-0.4531566, 51.4709959], // London
getTargetPosition: f => f.geometry.coordinates,
getSourceColor: [0, 128, 200],
getTargetColor: [200, 0, 80],
getWidth: 1
})
]
});
export default function Map() {
const { isLoaded } = useJsApiLoader({
id: 'lensmap',
googleMapsApiKey: "YOUR_API_KEY"
})
const onLoad = React.useCallback(function callback(map) {
deckOverlay.setMap(map)
}, [])
const onUnmount = React.useCallback(function callback(map) {
}, [])
return isLoaded ? (
<GoogleMap
mapContainerStyle={getWindowDimensions()}
center={center}
zoom={10}
onLoad={onLoad}
onUnmount={onUnmount}
>
<></>
</GoogleMap>
) : <></>
}
As you can see GoogleMapsOverlay receives a markers object in it's constructor, here I would get my markers doing a call to an API using axios but everything that I've tested ends in a 500 code when loading the page.
I assume that you're asking for a way to fetch the markers and make everything load in the correct order. I think you could store the deckOverlay instance in a ref, fetch the markers in a useEffect hook, update the layers with the markers data, and set a flag to hold from rendering the map until the layers are updated.
import React, { useState, useRef, useEffect, useCallback } from "react";
import { GoogleMap, useJsApiLoader } from "#react-google-maps/api";
import { GoogleMapsOverlay } from "#deck.gl/google-maps";
import { GeoJsonLayer, ArcLayer } from "#deck.gl/layers";
import axios from "axios";
const hasWindow = typeof window !== "undefined";
function getWindowDimensions() {
const width = hasWindow ? window.innerWidth : null;
const height = hasWindow ? window.innerHeight : null;
return {
width,
height,
};
}
const center = {
lat: 51.509865,
lng: -0.118092,
};
export default function Map() {
const { isLoaded } = useJsApiLoader({
id: "lensmap",
googleMapsApiKey: "AIzaSyBmSBtlYQLH8jvAxrdgZErUdtdWLEs40gk",
});
const [markersLoaded, setMarkersLoaded] = useState(false);
const deckOverlay = useRef(new GoogleMapsOverlay({ layers: [] }));
const fecthMarkers = useCallback(async () => {
try {
const response = await axios.get(`someapi.com/markers`);
// assuming API response will have a markers field
const markers = response.data.markers;
deckOverlay.current.setProps({
layers: [
new GeoJsonLayer({
id: "airports",
data: markers,
filled: true,
pointRadiusMinPixels: 2,
opacity: 1,
pointRadiusScale: 2000,
getRadius: (f) => 11 - f.properties.scalerank,
getFillColor: [200, 0, 80, 180],
pickable: true,
autoHighlight: true,
}),
new ArcLayer({
id: "arcs",
data: markers,
dataTransform: (d) =>
d.features.filter((f) => f.properties.scalerank < 4),
getSourcePosition: (f) => [-0.4531566, 51.4709959], // London
getTargetPosition: (f) => f.geometry.coordinates,
getSourceColor: [0, 128, 200],
getTargetColor: [200, 0, 80],
getWidth: 1,
}),
],
});
setMarkersLoaded(true);
} catch (e) {
// TODO: show some err UI
console.log(e);
}
}, []);
useEffect(() => {
fecthMarkers();
},[]);
const onLoad = React.useCallback(function callback(map) {
deckOverlay.current?.setMap(map);
}, []);
const onUnmount = React.useCallback(function callback(map) {
deckOverlay.current?.finalize();
}, []);
return markersLoaded && isLoaded ? (
<GoogleMap
mapContainerStyle={getWindowDimensions()}
center={center}
zoom={10}
onLoad={onLoad}
onUnmount={onUnmount}
>
<></>
</GoogleMap>
) : (
<></>
);
}
While it's a good idea to use a ref in most cases, it's not technically needed in this case, if there's just 1 instance of the component on the page. The important part is that you use an effect, which can run any JS and interact with any function / variable that is in scope.
Also important to know is that you need to add setMarkersLoaded(true); at the end to ensure a new render happens, if you want one to happen. If you don't need a render to happen (e.g. here if the map was already displayed regardless of whether the markers loaded), you can remove this part.
diedu's answer uses useCallback to create the async handler (fetchMarkers) used in useEffect, however you don't need to use this hook here. The function is written to ever be called just once, and is not passed to any component. useCallback is only for when you find a new function being created causes a component to re-render that otherwise wouldn't.
It's better to define the data fetching function outside of the component, so that you can keep the effect code simple and readable. You can even map it to layers in that function, and so remove another large chunk of logic out of your Map component.
useEffect(() => {
(async () {
const layers = await fetchMarkerLayers();
deckOverlay.current.setProps({layers});
setMarkersLoaded(true);
})();
},[]);
Because the argument of useEffect can not be an async function, you need put a self invoking async function inside. If you don't like that syntax, you could also chain promises with .then. Both syntaxes are a bit hairy, but because we extracted the complex logic out of the component, it's still readable.
Full code
I kept some parts of diedu's snippet, like how the ref is used, as they didn't need changes.
import React, { useState, useRef, useEffect, useCallback } from "react";
import { GoogleMap, useJsApiLoader } from "#react-google-maps/api";
import { GoogleMapsOverlay } from "#deck.gl/google-maps";
import { GeoJsonLayer, ArcLayer } from "#deck.gl/layers";
import axios from "axios";
const hasWindow = typeof window !== "undefined";
function getWindowDimensions() {
const width = hasWindow ? window.innerWidth : null;
const height = hasWindow ? window.innerHeight : null;
return {
width,
height,
};
}
const center = {
lat: 51.509865,
lng: -0.118092,
};
const fetchMarkerLayers = async () => {
try {
const response = await axios.get(`someapi.com/markers`);
// assuming API response will have a markers field
const { markers } = response.data;
return [
new GeoJsonLayer({
id: "airports",
data: markers,
filled: true,
pointRadiusMinPixels: 2,
opacity: 1,
pointRadiusScale: 2000,
getRadius: (f) => 11 - f.properties.scalerank,
getFillColor: [200, 0, 80, 180],
pickable: true,
autoHighlight: true,
}),
new ArcLayer({
id: "arcs",
data: markers,
dataTransform: (d) =>
d.features.filter((f) => f.properties.scalerank < 4),
getSourcePosition: (f) => [-0.4531566, 51.4709959], // London
getTargetPosition: (f) => f.geometry.coordinates,
getSourceColor: [0, 128, 200],
getTargetColor: [200, 0, 80],
getWidth: 1,
}),
]
} catch (e) {
// TODO: show some err UI
console.log(e);
}
};
export default function Map() {
const { isLoaded } = useJsApiLoader({
id: "lensmap",
googleMapsApiKey: "AIzaSyBmSBtlYQLH8jvAxrdgZErUdtdWLEs40gk",
});
const [markersLoaded, setMarkersLoaded] = useState(false);
const deckOverlay = useRef(new GoogleMapsOverlay({ layers: [] }));
useEffect(() => {
// Use a self invoking async function because useEffect's argument function cannot be async.
// Alternatively you can chain a regular Promise with `.then(layers => ...)`.
(async () {
const layers = await fetchMarkerLayers();
deckOverlay.current.setProps({layers});
setMarkersLoaded(true);
})();
},[]);
const onLoad = React.useCallback(function callback(map) {
deckOverlay.current?.setMap(map);
}, []);
const onUnmount = React.useCallback(function callback(map) {
deckOverlay.current?.finalize();
}, []);
return markersLoaded && isLoaded ? (
<GoogleMap
mapContainerStyle={getWindowDimensions()}
center={center}
zoom={10}
onLoad={onLoad}
onUnmount={onUnmount}
>
<></>
</GoogleMap>
) : (
<></>
);
}
I am trying to build a react app and I need one component for simple handwriting and drawing on mobile/touch devices.
The problem im am facing is that the TouchMoveEvent is not fired upon small movements. Therefore it gets pretty hard if someone has a small handwriting. Sometimes some parts of letters or numbers are missing because the touchmoveevent was not fired and subsequently no line has been drawed.
Has anyone an idea how to lower the treshhold to fire the touchmoveevent or has someone a different approach to this? The goal is just a very simple mobile drawing app but the touchmoveevent is not sufficient for very detailed and small drawings as it is not fired upon small movements.
This is my code so far:
import React from "react";
const Immutable = require('immutable');
class DrawArea extends React.Component {
constructor(sized) {
super();
this.state = {
lines: new Immutable.List(),
isDrawing: false
};
console.log(sized)
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleTouchStart = this.handleTouchStart.bind(this);
this.handleTouchMove = this.handleTouchMove.bind(this);
this.handleTouchEnd = this.handleTouchEnd.bind(this);
}
componentDidMount() {
document.addEventListener("mouseup", this.handleMouseUp);
document.addEventListener("touchend", this.handleTouchEnd);
}
componentWillUnmount() {
document.removeEventListener("mouseup", this.handleMouseUp);
document.removeEventListener("touchend", this.handleTouchEnd);
}
handleMouseDown(mouseEvent) {
if (mouseEvent.button != 0) {
return;
}
const point = this.relativeCoordinatesForEvent(mouseEvent);
this.setState(prevState => ({
lines: prevState.lines.push(new Immutable.List([point])),
isDrawing: true
}));
}
handleMouseMove(mouseEvent) {
if (!this.state.isDrawing) {
return;
}
const point = this.relativeCoordinatesForEvent(mouseEvent);
this.setState(prevState => ({
lines: prevState.lines.updateIn([prevState.lines.size - 1], line => line.push(point))
}));
}
handleMouseUp() {
this.setState({ isDrawing: false });
}
handleTouchStart(e) {
console.log("s")
let touch = e.touches[0];
const point = this.relativeCoordinatesForEvent(touch);
this.setState(prevState => ({
lines: prevState.lines.push(new Immutable.List([point])),
isDrawing: true
}));
}
handleTouchMove(e) {
console.log("m")
if (!this.state.isDrawing) {
return;
}
let touch = e.touches[0];
const point = this.relativeCoordinatesForEvent(touch)
this.setState(prevState => ({
lines: prevState.lines.updateIn([prevState.lines.size - 1], line => line.push(point))
}));
}
handleTouchEnd() {
console.log("e")
this.setState({ isDrawing: false });
}
relativeCoordinatesForEvent(mouseEvent) {
const boundingRect = this.refs.drawArea.getBoundingClientRect();
return new Immutable.Map({
x: mouseEvent.clientX - boundingRect.left,
y: mouseEvent.clientY - boundingRect.top,
});
}
relativeCoordinatesForTouchEvent(mouseEvent) {
const boundingRect = this.refs.drawArea.getBoundingClientRect();
return new Immutable.Map({
x: mouseEvent.clientX - boundingRect.left,
y: mouseEvent.clientY - boundingRect.top,
});
}
render() {
//console.log(this.state.lines)
//this.state.lines.map(s => console.log(s))
return (
<div
className="drawArea"
ref="drawArea"
onMouseDown={this.handleMouseDown}
onMouseMove={this.handleMouseMove}
onTouchStart={this.handleTouchStart}
onTouchMove={this.handleTouchMove}
onTouch
>
<Drawing sized={this.props.sized} lines={this.state.lines} />
</div>
);
}
}
function Drawing({ lines, sized }) {
return (
<svg className="drawing">
{lines.map((line, index) => (
<DrawingLine key={index} sized={sized} line={line} />
))}
</svg>
);
}
function DrawingLine({ line, sized }) {
let multi = sized ? 1.0 : 0.5;
const pathData = "M " +
line
.map(p => {
return `${p.get('x')*multi} ${p.get('y')*multi}`;
})
.join(" L ");
return <path className="path" d={pathData} />;
}
export default DrawArea;
´´´
I solved the problem with this thread:
javascript, is there a way to set a (smaller) threshold on touchmove event
using the PointerMoveEvent
I'm trying to receive fog effect in React. The main idea is that I have two components: first component handles with updating of coordinates of clouds and their velocity, the second component is responsible for one cloud. I have problem with moving of clouds, if I don't clear canvas I can see track of every cloud, if I apply canvas.clear I can't see anything. Do you have any tip, where I should place clear canvas.clear or do you have other ideas?
The first component:
import React from 'react';
import styled from 'styled-components';
import FogPiece from './Fog-Piece.jsx';
const CanvasContext = React.createContext();
const FogDiv = styled.div`
position: absolute;
width:100vw;
height:100vh;
`
class Fog extends React.Component{
constructor(props){
super(props);
this.canvas = React.createRef();
this.state = {
ctx: null,
parameters:[],
screenWidth : 0,
screenHeight: 0,
}
}
componentDidMount = () => {
Promise.all(this.newCoordinates()).then((paramArray) =>{
this.setState({
ctx: this.canvas.current.getContext('2d'),
screenWidth: this.canvas.current.parentNode.getBoundingClientRect().width,
screenHeight: this.canvas.current.parentNode.getBoundingClientRect().height,
parameters: paramArray
});
window.requestAnimationFrame(this.update)
})
}
newCoordinates = () => {
return(Array.from(Array(this.props.density).keys()).map(elem =>{
return new Promise (resolve => {
const params = {
x: this.random(0,this.state.screenWidth),
y: this.random(0,this.state.screenHeight),
velocityX: this.random(-this.props.maxVelocity, this.props.maxVelocity),
velocityY: this.random(-this.props.maxVelocity, this.props.maxVelocity)
}
resolve(params)
})
}))
}
updateCoordinates = () => {
return(this.state.parameters.map(elem =>{
return new Promise (resolve => {
elem = this.ifCross(elem.x, elem.y, elem.velocityX, elem.velocityY);
const params = {
x: elem.x + elem.velocityX,
y: elem.y + elem.velocityY,
velocityX: elem.velocityX,
velocityY: elem.velocityY
}
resolve(params)
})
}))
}
random = (min,max) => {
return Math.random()*(max - min) + min
}
ifCross = (x,y, velocityX, velocityY) => {
if (x > this.state.screenWidth){
x = this.state.screenWidth
velocityX = - velocityX
}
if (x < 0){
x = 0
velocityX = - velocityX
}
if (y > this.state.screenHeight){
y = this.state.screenHeight
velocityY = - velocityY
}
if (y < 0){
y = 0
velocityY = - velocityY
}
return {x:x, y:y, velocityX:velocityX, velocityY:velocityY }
}
update = () => {
Promise.all(this.updateCoordinates()).then((paramArray) =>{
//here is the problem
// this.state.ctx.clearRect(0,0,this.state.screenWidth, this.state.screenHeight)
this.setState({
parameters: paramArray,
});
window.requestAnimationFrame(this.update)
})
}
render(){
return(
<FogDiv>
<canvas width={this.state.screenWidth} height={this.state.screenHeight} ref = {this.canvas} >
{this.state.ctx && (
<CanvasContext.Provider value = {this.state.ctx}>
{this.state.parameters.map(param =>(
<FogPiece
x = {param.x}
y = {param.y}
/>
))}
</CanvasContext.Provider>
)}
</canvas>
</FogDiv>
)
}
}
export default Fog;
export {
CanvasContext
}
the second one:
import React from 'react';
import styled from 'styled-components';
import {CanvasContext} from './Fog.jsx';
class FogPiece extends React.Component{
constructor(props){
super(props);
this.state = {
image:'https://media.istockphoto.com/vectors/sample-red-square-grunge-textured-isolated-stamp-vector-id471401412',
}
}
random(min,max){
return Math.random()*(max - min) + min
}
render(){
return(
<CanvasContext.Consumer>
{ctx => {
console.log("x", "y", this.props)
const img = new Image();
img.src = this.state.image;
img.onload = () => {
ctx.drawImage(img,
this.props.x,
this.props.y,
40,
40)
}
}}
</CanvasContext.Consumer>
)
}
}
export default FogPiece;
I decide to reuse a component that I thought would work for my new application that is pulling in a third-party API.
The reusable component in question is iterating this.props.data.map() which is evaluating as being undefined in my components/Swipe.js file:
import React, { Component } from "react";
import {
View,
Animated,
PanResponder,
Dimensions,
LayoutAnimation,
UIManager
} from "react-native";
const SCREEN_WIDTH = Dimensions.get("window").width;
const SWIPE_THRESHOLD = 0.25 * SCREEN_WIDTH;
const SWIPE_OUT_DURATION = 250;
class Swipe extends Component {
static defaultProps = {
onSwipeRight: () => {},
onSwipeLeft: () => {}
};
constructor(props) {
super(props);
const position = new Animated.ValueXY();
const panResponder = PanResponder.create({
onStartShouldSetPanResponder: (event, gestureState) => true,
onPanResponderMove: (event, gestureState) => {
position.setValue({ x: gestureState.dx, y: gestureState.dy });
},
onPanResponderRelease: (event, gestureState) => {
if (gestureState.dx > SWIPE_THRESHOLD) {
this.forceSwipe("right");
} else if (gestureState.dx < -SWIPE_THRESHOLD) {
this.forceSwipe("left");
} else {
this.resetPosition();
}
}
});
this.state = { panResponder, position, index: 0 };
}
componentWillReceiveProps(nextProps) {
if (nextProps.data !== this.props.data) {
this.setState({ index: 0 });
}
}
componentWillUpdate() {
UIManager.setLayoutAnimationEnabledExperimental &&
UIManager.setLayoutAnimationEnabledExperimental(true);
LayoutAnimation.spring();
}
forceSwipe(direction) {
const x = direction === "right" ? SCREEN_WIDTH : -SCREEN_WIDTH;
Animated.timing(this.state.position, {
toValue: { x, y: 0 },
duration: SWIPE_OUT_DURATION
}).start(() => this.onSwipeComplete(direction));
}
onSwipeComplete(direction) {
const { onSwipeLeft, onSwipeRight, data } = this.props;
const item = data[this.state.index];
direction === "right" ? onSwipeRight(item) : onSwipeLeft(item);
this.state.position.setValue({ x: 0, y: 0 });
this.setState({ index: this.state.index + 1 });
}
resetPosition() {
Animated.spring(this.state.position, {
toValue: { x: 0, y: 0 }
}).start();
}
getCardStyle() {
const { position } = this.state;
const rotate = position.x.interpolate({
inputRange: [-SCREEN_WIDTH * 1.5, 0, SCREEN_WIDTH * 1.5],
outputRange: ["-120deg", "0deg", "120deg"]
});
return {
...position.getLayout(),
transform: [{ rotate }]
};
}
renderCards() {
console.log(this.props);
if (this.state.index >= this.props.data.length) {
return this.props.renderNoMoreCards();
}
return this.props.data
.map((item, i) => {
if (i < this.state.index) {
return null;
}
if (i === this.state.index) {
return (
<Animated.View
key={item[this.props.id]}
style={[this.getCardStyle(), styles.cardStyle]}
{...this.state.panResponder.panHandlers}
>
{this.props.renderCard(item)}
</Animated.View>
);
}
return (
<Animated.View
key={item[this.props.id]}
style={[styles.cardStyle, { top: 10 * (i - this.state.index) }]}
>
{this.props.renderCard(item)}
</Animated.View>
);
})
.reverse();
}
render() {
return <View>{this.renderCards()}</View>;
}
}
const styles = {
cardStyle: {
position: "absolute",
width: SCREEN_WIDTH
}
};
export default Swipe;
I am unclear why this is happening since I do get back a payload: data in my action creator:
export const fetchJobs = (region, callback) => async dispatch => {
try {
const url =
JOB_ROOT_URL +
JOB_QUERY_PARAMS.key +
"&method=" +
JOB_QUERY_PARAMS.method +
"&category=" +
JOB_QUERY_PARAMS.keyword +
"&format=" +
JOB_QUERY_PARAMS.format;
let { data } = await axios.get(url);
dispatch({ type: FETCH_JOBS, payload: data });
callback();
} catch (e) {
console.log(e);
}
};
So why is data evaluating as undefined in my reusable component?
It's being called here in DeckScreen.js:
import React, { Component } from "react";
import { View, Text } from "react-native";
import { connect } from "react-redux";
import { MapView } from "expo";
import { Card, Button } from "react-native-elements";
import Swipe from "../components/Swipe";
class DeckScreen extends Component {
renderCard(job) {
return (
<Card title={job.title}>
<View style={styles.detailWrapper}>
<Text>{job.company}</Text>
<Text>{job.post_date}</Text>
</View>
<Text>
{job.description.replace(/<span>/g, "").replace(/<\/span>/g, "")}
</Text>
</Card>
);
}
render() {
return (
<View>
<Swipe data={this.props.jobs} renderCard={this.renderCard} />
</View>
);
}
}
const styles = {
detailWrapper: {
flexDirection: "row",
justifyContent: "space-around",
marginBottom: 10
}
};
function mapStateToProps({ jobs }) {
return { jobs: jobs.listing };
}
export default connect(mapStateToProps)(DeckScreen);
The button I am pressing that gives me this error is in the MapScreen screen:
import React, { Component } from "react";
import { View, Text, ActivityIndicator } from "react-native";
import { Button } from "react-native-elements";
import { MapView } from "expo";
import { connect } from "react-redux";
import * as actions from "../actions";
class MapScreen extends Component {
state = {
region: {
longitude: 30.2672,
latitude: 97.7431,
longitudeDelta: 0.04,
latitudeDelta: 0.09
}
};
onButtonPress = () => {
this.props.fetchJobs(this.state.region, () => {
this.props.navigation.navigate("deck");
});
};
getLocationHandler = () => {
navigator.geolocation.getCurrentPosition(pos => {
const currentCoords = {
longitude: pos.coords.longitude,
latitude: pos.coords.latitude
};
this.goToLocation(currentCoords);
});
};
goToLocation = coords => {
this.map.animateToRegion({
...this.state.region,
longitude: coords.longitude,
latitude: coords.latitude
});
this.setState(prevState => {
return {
region: {
...prevState.region,
longitude: coords.longitude,
latitude: coords.latitude
}
};
});
};
render() {
return (
<View style={{ flex: 1 }}>
<MapView
initialRegion={this.state.region}
style={{ flex: 1 }}
ref={ref => (this.map = ref)}
/>
<View style={styles.buttonContainer}>
<Button
title="Search This Area"
icon={{ name: "search" }}
onPress={this.onButtonPress}
/>
</View>
<View>
<Button
title="My Location"
icon={{ name: "map" }}
onPress={this.getLocationHandler}
/>
</View>
</View>
);
}
}
const styles = {
buttonContainer: {
position: "absolute",
bottom: 50,
left: 0,
right: 0
}
};
export default connect(
null,
actions
)(MapScreen);
This should be an array of objects as verified here:
And in my reducer I have:
import { FETCH_JOBS } from "../actions/types";
const INITIAL_STATE = {
listing: []
};
export default function(state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_JOBS:
return action.payload;
default:
return state;
}
}
I added some verbose error handling and this is what I got back:
[02:25:28] fetchJobs Action Error: Given action "fetch_jobs", reducer
"jobs" returned undefined. To ignore an action, you must explicitly
return the previous state. If you want this reducer to hold no value,
you can return null instead of undefined.
So it seems like the problem is in the jobs_reducer:
import { FETCH_JOBS } from "../actions/types";
const INITIAL_STATE = {
listing: []
};
export default function(state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_JOBS:
return action.payload;
default:
return state;
}
}
I don't know if I am just too exhausted at this point, but I have tried listings: [], I have tried listing: [], I am out of ideas of how to get this reducer to not return undefined because even when I do this:
import { FETCH_JOBS } from "../actions/types";
// const INITIAL_STATE = {
// listing: []
// };
export default function(state = null, action) {
switch (action.type) {
case FETCH_JOBS:
return action.payload;
default:
return state;
}
}
I get the same error message.
My idea with creating an INITIAL_STATE and setting it to listing: [] is to ensure I could map over this array and never worry about the case where I have not yet fetched the list of jobs.
So I am perplexed as to exactly where I am getting this undefined since I did set the initial state to null and I was still getting that error.
So in the process of debugging I then tried this:
import { FETCH_JOBS } from "../actions/types";
// const INITIAL_STATE = {
// listing: []
// };
export default function(state = null, action) {
console.log("action is", action);
switch (action.type) {
case FETCH_JOBS:
return action.payload;
default:
return state;
}
}
And got that the payload is undefined:
Please check your inputs.
[09:39:38] action is Object {
[09:39:38] "payload": undefined,
[09:39:38] "type": "fetch_jobs",
[09:39:38] }
I have hit a wall here. I did a whole refactor to my jobs action creator and logged out the payload property:
export const fetchJobs = (region, distance = 10) => async dispatch => {
try {
const url = buildJobsUrl();
let job_list = await axios.get(url);
job_list = locationify(
region,
console.log(job_list.data.listings.listing),
job_list.data.listings.listing,
distance,
(obj, coords) => {
obj.company.location = { ...obj.company.location, coords };
return obj;
}
);
dispatch({ type: FETCH_JOBS, payload: job_list });
} catch (e) {
console.log("fetchJobs Action Error:", e.message);
}
};
The console.log(job_list.data.listings.listing) logged out the data to my terminal successfully and yet my payload property is still undefined, how is that possible?
I got the action creator and reducer working by refactoring the action creator to just this:
import axios from "axios";
import { Location } from "expo";
import qs from "qs";
import { FETCH_JOBS } from "./types";
// import locationify from "../tools/locationify";
const JOB_ROOT_URL = "https://authenticjobs.com/api/?";
const JOB_QUERY_PARAMS = {
api_key: "<api_key>",
method: "aj.jobs.search",
perpage: "10",
format: "json",
keywords: "javascript"
};
const buildJobsUrl = zip => {
const query = qs.stringify({ ...JOB_QUERY_PARAMS });
return `${JOB_ROOT_URL}${query}`;
};
export const fetchJobs = (region, callback) => async dispatch => {
try {
let zip = await Location.reverseGeocodeAsync(region);
const url = buildJobsUrl(zip);
console.log(url);
let { data } = await axios.get(url);
dispatch({ type: FETCH_JOBS, payload: data });
callback();
} catch (e) {
console.error(e);
}
};
So the problem is no longer there in theory, right. Then, when I bring in the Swipe.js component, the problem returns, in particular the problem seems to be with this code here:
renderCards() {
if (this.state.index >= this.props.data.length) {
return this.props.renderNoMoreCards();
}
return this.props.data
.map((item, i) => {
if (i < this.state.index) {
return null;
}
if (i === this.state.index) {
return (
<Animated.View
key={item[this.props.id]}
style={[this.getCardStyle(), styles.cardStyle]}
{...this.state.panResponder.panHandlers}
>
{this.props.renderCard(item)}
</Animated.View>
);
}
return (
<Animated.View
key={item[this.props.id]}
style={[styles.cardStyle, { top: 10 * (i - this.state.index) }]}
>
{this.props.renderCard(item)}
</Animated.View>
);
})
.reverse();
}
This is where I start to hit a roadblock again.
Props are not available immediatelly from redux store on render, it comes asyncrounously.
To select data from redux store better to use save navigation:
const mapStateToProps = state => ({
jobs: state && state.jobs && state.jobs.listing
})
Than again on render to check if data exists or not:
...
render() {
const { jobs } = this.props;
return (
<View>
{jobs && <Swipe data={jobs} renderCard={this.renderCard} />}
</View>
}
...
renderCards() {
const { data } = this.props;
return data && data.map((item, index) => {
...
map function generally iterate through array-object. You are trying to iterate through a non-array object. So first check the type of the object using typeof(variable) then use the function.
Looks like what helped was refactoring my jobs_reducer file from:
import { FETCH_JOBS } from "../actions/types";
const INITIAL_STATE = {
listing: []
};
export default function(state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_JOBS:
return action.payload;
default:
return state;
}
}
to this:
export default function(state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_JOBS:
const { listings } = action.payload;
return { ...state, listing: listings.listing };
default:
return state;
}
}