React stale state with useEffect loop - javascript

Given the following component:
import { useEffect, useState } from "react"
const Test = () => {
const [thing, setThing] = useState({ x: 0, y: 0 });
useEffect(() => {
gameLoop();
}, []);
const gameLoop = () => {
const p = {...thing};
p.x++;
setThing(p);
setTimeout(gameLoop, 1000);
}
return (
<div>
</div>
)
}
export default Test;
Every second a new line is printed into the console. The expected output should of course be:
{ x: 1, y: 0 }
{ x: 2, y: 0 }
{ x: 3, y: 0 }
...etc
What is instead printed is:
{ x: 0, y: 0 }
{ x: 0, y: 0 }
{ x: 0, y: 0 }
I've come to the conclusion as this is because of a stale state. Some solutions I've found and tried are:
The dependency array
Adding thing to the dependency array causes it to be called ridiculously fast. Adding thing.x to the array also does the same.
Changing the setState call
One post I found mentioned changing the setState to look like so:
setState(thing => {
return {
...thing.
x: thing.x+1,
y: thing.y
});
Which changed nothing.
I have found a lot of posts about clocks/stopwatches and how to fix this issue with primitive value states but not objects.
What is my best course of action to fix the behavior while maintaining that it only runs once a second?
EDIT: The comments seem to be stating I try something like this:
import { useEffect, useState } from "react"
const Test = () => {
const [thing, setThing] = useState({ x: 0, y: 0 });
useEffect(() => {
setInterval(gameLoop, 1000);
}, []);
const gameLoop = () => {
console.log(thing)
const p = {...thing};
p.x++;
setThing(prevP => ({...prevP, x: prevP.x+1}));
}
return (
<div>
</div>
)
}
export default Test;
This still prints out incorrectly:
{ x: 0, y: 0 }
{ x: 0, y: 0 }
{ x: 0, y: 0 }

I think you can use this solution:
useEffect(() => {
setInterval(() => {
setThing((prev) => ({
...prev,
x: prev.x + 1,
}));
}, 1000);
}, []);
I suggest to control setInterval with the clearInterval when component is destroied; like this:
useEffect(() => {
const interval = setInterval(() => {
setThing((prev) => ({
...prev,
x: prev.x + 1,
}));
}, 1000);
return () => {
clearInterval(interval);
};
}, []);

Related

Why is my Animated.Image not rotating (React-Native)

I'm new to react so please be nice,
I'm trying to animate my compass, so that every time the userLocation is updated, the arrow (in my code the png of the animated image) is rotated at the given angle (here rotation) so that it points at another location. For some reason, it seems like the rotation passed to the Animated.Image remains 0, because the image never rotates. Can someone land me a hand real quick.
Here's my code:
import {
Alert,
Animated,
Easing,
Linking,
StyleSheet,
Text,
View,
} from "react-native";
import React, { useEffect, useRef, useState } from "react";
import * as Location from "expo-location";
import * as geolib from "geolib";
import { COLORS } from "../../assets/Colors/Colors";
export default function DateFinder() {
const [hasForegroundPermissions, setHasForegroundPermissions] =
useState(null);
const [userLocation, setUserLocation] = useState(null);
const [userHeading, setUserHeading] = useState(null);
const [angle, setAngle] = useState(0);
const rotation = useRef(new Animated.Value(0)).current;
useEffect(() => {
const AccessLocation = async () => {
function appSettings() {
console.warn("Open settigs pressed");
if (Platform.OS === "ios") {
Linking.openURL("app-settings:");
} else RNAndroidOpenSettings.appDetailsSettings();
}
const appSettingsALert = () => {
Alert.alert(
"Allow Wassupp to Use your Location",
"Open your app settings to allow Wassupp to access your current position. Without it, you won't be able to use the love compass",
[
{
text: "Cancel",
onPress: () => console.warn("Cancel pressed"),
},
{ text: "Open settings", onPress: appSettings },
]
);
};
const foregroundPermissions =
await Location.requestForegroundPermissionsAsync();
if (
foregroundPermissions.canAskAgain == false ||
foregroundPermissions.status == "denied"
) {
appSettingsALert();
}
setHasForegroundPermissions(foregroundPermissions.status === "granted");
if (foregroundPermissions.status == "granted") {
const location = await Location.watchPositionAsync(
{
accuracy: Location.Accuracy.BestForNavigation,
activityType: Location.ActivityType.Fitness,
distanceInterval: 0,
},
(location) => {
setUserLocation(location);
}
);
const heading = await Location.watchHeadingAsync((heading) => {
setUserHeading(heading.trueHeading);
});
}
};
AccessLocation().catch(console.error);
}, []);
useEffect(() => {
if (userLocation != null) {
setAngle(getBearing() - userHeading);
rotateImage(angle); // Here's the call to the rotateImage function that should cause the value of rotation to be animated
}
}, [userLocation]);
const textPosition = JSON.stringify(userLocation);
const getBearing = () => {
const bearing = geolib.getGreatCircleBearing(
{
latitude: userLocation.coords.latitude,
longitude: userLocation.coords.longitude,
},
{
latitude: 45.47307231766645,
longitude: -73.86611198944459,
}
);
return bearing;
};
const rotateImage = (angle) => {
Animated.timing(rotation, {
toValue: angle,
duration: 1000,
easing: Easing.bounce,
useNativeDriver: true,
}).start();
};
return (
<View style={styles.background}>
<Text>{textPosition}</Text>
<Animated.Image
source={require("../../assets/Compass/Arrow_up.png")}
style={[styles.image, { transform: [{ rotate: `${rotation}deg` }] }]} // this is where it should get rotated but it doesn't for some reason
/>
</View>
);
}
const styles = StyleSheet.create({
background: {
backgroundColor: COLORS.background_Pale,
flex: 1,
// justifyContent: "flex-start",
//alignItems: "center",
},
image: {
flex: 1,
// height: null,
// width: null,
//alignItems: "center",
},
scrollView: {
backgroundColor: COLORS.background_Pale,
},
});
Your error is here
useEffect(() => {
if (userLocation != null) {
setAngle(getBearing() - userHeading);
rotateImage(angle); // Here's the call to the rotateImage function that should cause the value of rotation to be animated
}
}, [userLocation]);
The angle will be updated on the next render, so the rotation you do will always be a render behind. You could either store the result of getBearing and setAngle to that value as well as provide that value to rotateImage:
useEffect(() => {
if (userLocation != null) {
const a = getBearing() -userHeading;
setAngle(a);
rotateImage(a); // Here's the call to the rotateImage function that should cause the value of rotation to be animated
}
}, [userLocation]);
or you could use useEffect and listen for angle changes:
useEffect(() => {
rotateImage(angle)
}, [angle]);

How can I make this infinite? I want to move the first element of an array to the last position using React Js

I creating this automatic infinite slider in React Js but it is not infinite. I am thinking that the best way to make it infinite is to move the first element to the end and make that in the setInterval in that way my array is always exchanging positions.
export const Slider = ({ images }) => {
const [position, setPosition] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setPosition(prev => prev + 0.1);
}, 1000);
}, []);
const containerVar = {
hidden: {
x: 0,
},
show: {
x: -250 * position,
transition: { repeat: Infinity, duration: 2, ease: 'linear' },
},
};
return (
<Container
as={motion.div}
variants={containerVar}
initial='hidden'
animate='show'
>
{images.map(({ name, ...imagesData }) => (
<CardClients key={name} name={name} {...imagesData} />
))}
</Container>
);
};

Infinite loop while using useEffect

I am looping through the contents of the following array:
chosenBets: [
{
id: 2,
label: 2,
odd: 4.8,
team_home: "Liverpool",
team_away: "Sheffield United",
},
{
id: 2,
label: 2,
odd: 4.8,
team_home: "Liverpool",
team_away: "Sheffield United",
},
],
To get the total odds of each bet:
const getTotalOdds = () => {
let totalOdds = 0
chosenBets.forEach(bet => {
totalOdds += bet.odd
})
let newState = Object.assign({}, state)
newState.totalOdds = totalOdds
setState({ ...newState })
}
But when I call the function inside of useEffect it causes an infinite loop.
Full code:
import * as React from "react"
import { Frame, Stack, addPropertyControls, ControlType } from "framer"
import { Input } from "./Input"
interface Bets {
id: number
label: number
odd: number
team_home: string
team_away: string
}
interface IProps {
chosenBets: Bets[]
}
const style = {
span: {
display: "block",
},
}
const Row = ({ label, property }) => (
<div style={{ display: "flex", width: "100%" }}>
<span style={{ ...style.span, marginRight: "auto" }}>{label}</span>
<span style={{ ...style.span }}>{property}</span>
</div>
)
export const BetCalculator: React.FC<IProps> = props => {
const { chosenBets } = props
const [state, setState] = React.useState({
stake: 10,
totalOdds: 0,
potentialPayout: 0,
})
const { stake, totalOdds } = state
const onChange = e => {
let newState = Object.assign({}, state)
newState.stake = Number(e.value)
setState({ ...newState })
}
const getTotalOdds = () => {
let totalOdds = 0
chosenBets.forEach(bet => {
totalOdds += bet.odd
})
let newState = Object.assign({}, state)
newState.totalOdds = totalOdds
setState({ ...newState })
}
const getPayout = () => {
let potentialPayout = totalOdds * stake
console.log(potentialPayout, "potentialPayout")
// let newState = Object.assign({}, state)
// newState.potentialPayout = potentialPayout
// setState({ ...newState })
}
React.useEffect(() => {
getTotalOdds()
getPayout()
}, [state])
return (
<Stack alignment="end" backgroundColor="white">
<Row
label="stake"
property={<Input onChange={onChange} value={stake} />}
/>
<Row label="Odds" property={totalOdds} />
</Stack>
)
}
BetCalculator.defaultProps = {
chosenBets: [
{
id: 2,
label: 2,
odd: 4.8,
team_home: "Liverpool",
team_away: "Sheffield United",
},
{
id: 2,
label: 2,
odd: 4.8,
team_home: "Liverpool",
team_away: "Sheffield United",
},
],
}
My questions are as follows:
What is causing this?
How do I resolve?
You can divide state into smaller chunks:
const [stake, setStake] = useState(10);
const [totalOdds, setTotalOdds] = useState(0);
const [potentialPayout, setPotentialPayout] = useState(0);
Easier to manage.
If your useEffect is called every time state has changed, and on the other hand every useEffect returns new state object - well. Infinite loop.
Just put the calculations in the onChange function and you won't need useEffect at all (or just use useEffect to cause side effects, like console.log);

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.

React update state array object single property

I have the following method:
addWidget = (index) => {
var currentState = this.state;
if(currentState.availableWidgets[index].pos === 'start'){
// add it at the start
for(var i = 0; i < currentState.widgets.length; i++){
this.setState({
widgets: [
...currentState.widgets,
currentState.widgets.x = 5
]
})
}
}
else {
var endX = currentState.widgets.reduce((endX, w) => endX + w.w, 0)
if (endX === 12) endX = 0
this.setState({
widgets: currentState.widgets.concat({
...currentState.availableWidgets[index],
i: uuid(),
x: endX,
y: Infinity,
})
})
}
console.log(currentState.widgets);
}
and the state is:
class DashboardContainer extends React.Component {
state = {
widgets: [],
availableWidgets: [
{
type: 'compliance-stats',
config: {
},
w: 1,
h: 1,
pos: 'start',
},
{
type: 'compliance-stats',
config: {
},
w: 3,
h: 2,
}
]
}
...
I am trying to update the "x" property of each object inside "widgets" by doing so:
for(var i = 0; i < currentState.widgets.length; i++){
this.setState({
widgets: [
...currentState.widgets,
currentState.widgets.x = 5
]
})
}
I am aware of setting the state inside a loop is not good at all. However I am currently getting an error.
I am importing widgets in:
const Dashboard = ({ widgets, onLayoutChange, renderWidget }) => {
const layouts = {
lg: widgets,
}
return (
<div>
<ResponsiveReactGridLayout
layouts={layouts}
onLayoutChange={onLayoutChange}
cols={{ lg: 12 }}
breakpoints={{lg: 1200}}>
{
widgets.map(
(widget) =>
<div key={widget.i}>
{renderWidget(widget)}
</div>
)
}
</ResponsiveReactGridLayout>
</div>
)
}
Probably better to change the widgets and then setState only once:
const changedWidgets = currentState.widgets.map(w => ({ ...w, x: 5 }));
this.setState({ widgets: changedWidgets });
The spread operator inside an array will concatenate a new value onto the array, so by seting widgets state to [ ...currentState.widgets, currentState.widgets.x = 5 ] what you're actually trying to do is currentState.widgets.concate(currentState.widgets.x = 5) which is throwing your error.
If you would like to modify the value inside an array you should map out the array and then modify the objects inside the array like so.
const widgets = currentState.widgets.map(widget => { ...widget, x: 5})
this.setState({ widgets })

Categories