JS/React Drawing Application with TouchMoveEvent - javascript

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

Related

React has detected a change in the order of Hooks called by ForwardRef(ContainerComponent)

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

Assigning ScrollTo value cause unexpected flickering/blinking on iOS devices

We recently worked on an auto-scrolling while freely swipeable component using React.js. The implementation idea is inspired by this article
And we've made something like this in React:
import React, { Component } from "react";
import PropTypes from "prop-types";
import "./AutoScroller.css";
const NUM_OF_CLONES = 10;
const AUTO_SCROLL_OFFSET = 1; // min offset of scrollTo is 1
const AUTO_SCROLL_INTERVAL = 32; // 1000 ms / 30 fps
export default class AutoScroller extends Component {
static propTypes = {
contents: PropTypes.array.isRequired,
itemWidth: PropTypes.number.isRequired,
numsOfItemsPerScreen: PropTypes.number.isRequired
};
constructor(props) {
super(props);
this.autoScrollerRef = React.createRef();
this.currentPosition = 0;
this.autoScrollTimer = null;
this.scrollingTimer = null;
/* boolean status */
this.isTouch = false;
this.isScrolling = false;
}
componentDidMount() {
this.startAutoScroll();
this.autoScrollerRef.current.addEventListener(
"touchstart",
this.touchStartHandler
);
this.autoScrollerRef.current.addEventListener(
"touchend",
this.touchEndHandler
);
this.autoScrollerRef.current.addEventListener("scroll", this.scrollHandler);
this.autoScrollerRef.current.addEventListener(
"contextmenu",
this.contextMenuHandler
);
}
componentWillUnmount() {
this.clearAutoScroll();
this.clearScrollingTimer();
this.autoScrollerRef.current.removeEventListener(
"touchstart",
this.touchStartHandler
);
this.autoScrollerRef.current.removeEventListener(
"touchend",
this.touchEndHandler
);
this.autoScrollerRef.current.removeEventListener(
"scroll",
this.scrollHandler
);
this.autoScrollerRef.current.removeEventListener(
"contextmenu",
this.contextMenuHandler
);
}
touchStartHandler = () => {
this.isTouch = true;
this.clearAutoScroll();
};
touchEndHandler = () => {
this.isTouch = false;
if (!this.isScrolling) {
this.currentPosition = this.autoScrollerRef.current.scrollLeft;
this.startAutoScroll();
}
};
scrollHandler = () => {
const {
contents: { length },
itemWidth
} = this.props;
this.isScrolling = true;
this.currentPosition = this.autoScrollerRef.current.scrollLeft;
const maxOffset = length * itemWidth;
if (this.currentPosition > maxOffset) {
const offset = this.currentPosition - maxOffset;
this.autoScrollerRef.current.scrollTo(offset, 0);
this.currentPosition = offset;
} else if (this.currentPosition <= 0) {
const offset = this.currentPosition + maxOffset;
this.autoScrollerRef.current.scrollTo(offset, 0);
this.currentPosition = offset;
}
/***
* note: there will be only one timer, and the timer is only created by the very last scroll
* only when the scroll event is not triggered anymore, the timer starts to get executed.
*/
if (this.scrollingTimer) {
clearTimeout(this.scrollingTimer);
}
this.scrollingTimer = setTimeout(() => {
this.isScrolling = false;
/***
* note: resume auto-scroll when the momentum scroll (after finger leaves) stalls the scroll
*/
if (!this.isTouch) {
this.startAutoScroll();
}
}, 300);
};
contextMenuHandler = (event) => {
event.preventDefault();
};
startAutoScroll = () => {
if (!this.autoScrollTimer) {
this.autoScrollTimer = setInterval(this.autoScroll, AUTO_SCROLL_INTERVAL);
}
};
clearAutoScroll = () => {
if (this.autoScrollTimer) {
clearInterval(this.autoScrollTimer);
this.autoScrollTimer = null;
}
};
clearScrollingTimer = () => {
if (this.scrollingTimer) {
clearTimeout(this.scrollingTimer);
this.scrollingTimer = null;
}
};
autoScroll = () => {
const {
contents: { length },
itemWidth,
numsOfItemsPerScreen
} = this.props;
if (this.currentPosition < 0) {
this.currentPosition = 0;
}
if (length > numsOfItemsPerScreen) {
const position = this.currentPosition + AUTO_SCROLL_OFFSET;
this.autoScrollerRef.current.scrollTo(position, 0);
const maxOffset = length * itemWidth;
if (this.currentPosition > maxOffset) {
const offset = this.currentPosition - maxOffset;
this.autoScrollerRef.current.scrollTo(offset, 0);
this.currentPosition = offset;
} else {
this.currentPosition = position;
}
}
};
getWrappedData = () => {
const { contents } = this.props;
const { length } = contents;
const numberOfClones = length < NUM_OF_CLONES ? length : NUM_OF_CLONES;
return [...contents, ...contents.slice(0, numberOfClones)];
};
render() {
const { itemGap, lineHeight } = this.props;
return (
<div className="auto-scroller" ref={this.autoScrollerRef}>
<ul>
{this.getWrappedData().map((content, index) => (
<Item
key={`auto-scroller-item-${index}`}
content={content}
itemGap={itemGap}
lineHeight={lineHeight}
/>
))}
</ul>
</div>
);
}
}
class Item extends Component {
static propTypes = {
content: PropTypes.object.isRequired,
itemGap: PropTypes.number,
lineHeight: PropTypes.number
};
render() {
const { content, itemGap = 10 } = this.props;
return (
<li
className="auto-scroller__item"
style={{ paddingRight: `${itemGap}px` }}
>
<div className="auto-scroller__item__content">
<img draggable={false} src={content.imgUrl} />
<div className="auto-scroller__item__content__title">
{content.title}
</div>
</div>
</li>
);
}
}
You can test with the demo from PlayCode (source code).
Just open the link with Safari on the iPhone.
What I observed was every time when it was on the boundary cases, the image started to flicker.
Further, if you swipe it with your finger forth and back on that point, the whole UI started to flicker. (see this screen recording) However, we didn't spot this glitch on Android devices.
Any possible solutions are welcome. Does anyone encounter something like this before?
removing overflow-y: hidden; and overflow-x: auto; from autoscroller.css
solved it on my end.
another solution would be to add z-index: 1; and scroll-behavior: smooth; to .auto-scroller
let me know if it worked!

rating component with custom icons

I am striving to make customized rating component. I know there are other libraries for it but I wanted to do it myself so i can understand the underlying process. However, i am struggling on custom component part. For default case, it's working fine. For custom component what I tried is allow developer to pass svg component of their desired icon and then show that icon as a rating component. Up to this, it's working but I have no idea on handling mouseover, mouseout functionality.
here is what I have tried
import React from "react";
const DefaultComponent = ({
ratingRef,
currentRating,
handleMouseOut,
handleMouseOver,
handleStarClick,
totalRating
}) => {
return (
<div
className="rating"
ref={ratingRef}
data-rating={currentRating}
onMouseOut={handleMouseOut}
>
{[...Array(+totalRating).keys()].map(n => {
return (
<span
className="star"
key={n + 1}
data-value={n + 1}
onMouseOver={handleMouseOver}
onClick={handleStarClick}
>
★
</span>
);
})}
</div>
);
};
const CustomComponent = ({
ratingRef,
currentRating,
handleMouseOut,
handleMouseOver,
handleStarClick,
totalRating,
ratingComponent
}) => {
return (
<>
{[...Array(+totalRating).keys()].map(n => {
return ratingComponent({ key: n });
})}
</>
);
};
const Rating = props => {
const { totalRating = 5, onClick, ratingComponent } = props;
const [currentRating, setCurrentRating] = React.useState(0);
React.useEffect(() => {
handleMouseOut();
}, []);
const handleMouseOver = ev => {
const stars = ev.target.parentElement.getElementsByClassName("star");
const hoverValue = ev.target.dataset.value;
Array.from(stars).forEach(star => {
star.style.color = hoverValue >= star.dataset.value ? "#FDC60A" : "#444";
});
};
const handleMouseOut = ev => {
const stars = ratingRef?.current?.getElementsByClassName("star");
stars &&
Array.from(stars).forEach(star => {
star.style.color =
currentRating >= star.dataset.value ? "#FDC60A" : "#444";
});
};
const handleStarClick = ev => {
let rating = ev.target.dataset.value;
setCurrentRating(rating); // set state so the rating stays highlighted
if (onClick) {
onClick(rating); // emit the event up to the parent
}
};
const ratingRef = React.useRef();
console.log("ratingComponent", ratingComponent);
return (
<>
{ratingComponent ? (
<CustomComponent
ratingRef={ratingRef}
currentRating={currentRating}
handleMouseOut={handleMouseOut}
handleMouseOver={handleMouseOver}
handleStarClick={handleStarClick}
totalRating={totalRating}
ratingComponent={ratingComponent}
/>
) : (
<DefaultComponent
ratingRef={ratingRef}
currentRating={currentRating}
handleMouseOut={handleMouseOut}
handleMouseOver={handleMouseOver}
handleStarClick={handleStarClick}
totalRating={totalRating}
/>
)}
</>
);
};
export default Rating;
I have created a sandbox as well.
https://codesandbox.io/s/stoic-almeida-yz7ju?file=/src/components/Rating/Rating.js:0-2731
Can anyone give me idea on how to make reusable rating component which can support custom icons for rating like in my case i needed exactly that gem and star icon?
I'd do it this way:
reuse your DefaultComponent to take in the icon
use classnames instead of selecting DOM elements; one for default and one for hover. Add fill or color properties for the hover/selected classname
use a hoverIndex state to track which element I'm hovering on
Improvement: colocate the hover events within the DefaultComponent since the are relevant to only the icon, and aren't needed in the parent container
I had trouble styling the star svg. Essentially, you need to override the fill color for it - but it seems to be a complicated one. If you can simplify it, or pass down the fill as prop into the svg and use it internally - either would work!
Sandbox: https://codesandbox.io/s/epic-pascal-j1d0c
Hope this helps!
const DefaultIcon = _ => "a"; // = '&#9733'
const DefaultComponent = ({
ratingComponent = DefaultIcon,
ratingRef,
currentRating,
handleStarClick,
totalRating
}) => {
const RatingIconComponent = ratingComponent;
const [hoverIndex, setHoverIndex] = React.useState(-Infinity);
const handleMouseOver = index => {
setHoverIndex(index);
};
const handleMouseOut = ev => {
setHoverIndex(-Infinity);
};
return (
<div
className="rating"
ref={ratingRef}
data-rating={currentRating}
onMouseOut={handleMouseOut}
>
{[...Array(+totalRating).keys()].map(n => {
const isFilled = n < currentRating || n <= hoverIndex;
return (
<span
className={`ratingIcon ${isFilled && "fill"}`}
key={n + 1}
data-value={n + 1}
onMouseOver={_ => handleMouseOver(n)}
onClick={_ => handleStarClick(n + 1)}
>
<RatingIconComponent />
</span>
);
})}
</div>
);
};
const Rating = props => {
const { totalRating = 5, onClick, ratingComponent } = props;
const [currentRating, setCurrentRating] = React.useState(0);
const handleStarClick = rating => {
setCurrentRating(rating); // set state so the rating stays highlighted
if (onClick) {
onClick(rating); // emit the event up to the parent
}
};
const ratingRef = React.useRef();
return (
<DefaultComponent
ratingComponent={ratingComponent}
ratingRef={ratingRef}
currentRating={currentRating}
handleStarClick={handleStarClick}
totalRating={totalRating}
/>
);
};
<Rating ratingComponent={Gem} />
/* for normal text */
.ratingIcon {
color: "gray";
}
.ratingIcon.fill {
color: yellow;
}
/* for svg */
.ratingIcon svg path {
fill: gray !important;
}
.ratingIcon.fill svg path {
fill: yellow !important;
}

React addEventListener having issues when the page rerenders

I have put together a infinite scroll to load more items when a user reaches the bottom of the screen. It works fine the first time but for some reason the eventListener seems to disappear when the first nextLink from redux is loaded.
My code:
import React, { Component } from "react";
import { connect } from "react-redux";
import * as teamsActions from "../../store/teams/actions";
import TeamCard from "../../common/teamCard/teamCard";
import ReactAI from "react-appinsights";
import WithLoading from "../../common/utils/withLoading";
import {
objectToArray,
sortArray
} from "../../assets/general_functions/objectsAndArrays";
import { faRubleSign } from "#fortawesome/free-solid-svg-icons";
class TeamsContainer extends Component {
_isMounted = false;
state = {
scrolling: false
};
componentDidMount() {
this._isMounted = true;
this.props.onFetchTeams();
this.scrollListener = window.addEventListener("scroll", this.handleScroll);
}
s;
componentWillUnmount() {
this._isMounted = false;
window.removeEventListener("scroll", this.handleScroll);
}
loadTeams = () => {
console.log("is teams loading?", this.props.loading);
if (this.props.loading === false) {
console.log("What is the nextLink", this.props.teams["#odata.nextLink"]);
this.props.onFetchMoreTeams(this.props.teams["#odata.nextLink"]);
}
};
loadMore = () => {
this.setState(
{
scrolling: true
},
this.loadTeams
);
};
handleScroll = () => {
const { scrolling } = this.state;
if (scrolling) return;
if (
typeof this.props.teams.value !== "undefined" ||
this.props.teams.value > 0
) {
console.log("value", this.props.teams.value);
const lastTeam = document.querySelector(
".team-card-wrapper:last-of-type"
);
// get the height of the current team, and get the height of the current position on screen.
const lastTeamOffset = lastTeam.offsetTop + lastTeam.clientHeight;
const pageOffset = window.pageYOffset + window.innerHeight;
const bottomOffset = 30;
if (pageOffset > lastTeamOffset - bottomOffset) {
this.loadMore();
}
}
};
render() {
let loading = "";
let error = "";
let teams = [];
let delay = 0;
let loadMoreButton = "";
// check whether the component is fetching data
let loader = "";
if (this.props.teamsLoading) {
loader = <WithLoading isLoading={true} />;
}
// check if there was an error
this.props.error && this.props.loading === false
? (error = <p>There was an error</p>)
: (error = "");
// reorder the teams and make teamCards of it.
if (this.props.teams["value"]) {
// convert the teams object to an array of objects.
// order it by sequence property.
teams = this.props.teams.value;
teams = objectToArray(this.props.teams["value"]);
teams = teams.sort(sortArray("sequence")).reverse();
teams = teams.map(team => {
if (delay === 300) {
delay = 0;
}
delay = delay + 75;
return (
<TeamCard
delay={delay}
id={team.id}
title={team.title}
description={team.description}
isFavorite={team.isFavorite}
memberCount={team.memberCount}
key={team.id}
/>
);
});
} else {
teams = loader = <WithLoading isLoading={true} />;
}
// this.props.teams["value"]);
return (
<React.Fragment>
<div className="App">
{loader == "" ? (
<div className="team-cards-wrapper">{teams}</div>
) : (
<div>{loader}</div>
)}
</div>
</React.Fragment>
);
}
}
const mapStateToProps = state => {
return {
error: state.teamsSlice.teamsError,
loading: state.teamsSlice.teamsLoading,
teams: state.teamsSlice.teams,
searchTerm: state.teamsSlice.searchTerm
};
};
const mapDispatchToProps = dispatch => {
return {
onFetchTeams: () => dispatch(teamsActions.fetchTeams()),
onFetchMoreTeams: teamsNextLink =>
dispatch(teamsActions.fetchMoreTeams(teamsNextLink))
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(ReactAI.withTracking(TeamsContainer));
My console (The value is printed on scroll. after loading it, it doesnt echo any value anymore):
Event handlers according to the console:
Redux store:
Edit:
I found that there was a problem in the state.scrolling. It's set to true but never reset after the data has loaded.
any help is much appreciated! Cheers
I think because you are having two componentWillUnMount. and this line
window.addEventListener("scroll", e => {
this.handleScroll(e);
});
seems to be a little wrong. it maybe window.addEventListener("scroll", this.handleScroll) only
- second thing is that I think you should use debounce on scroll event so it can be better for performance
I found out that the problem was setting the scrolling state back to false.
After fetching more redux items, i decided to use a timeout to debounce incoming similar api requests.
setTimeout(() => {
this.setState({ scrolling: false });
}, 300);

React props out of date in event handler

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.

Categories