I'm using the State Hook to update a Throttle setting for a robot based on keyboard commands (W,A,S,D). I have a maxThrottle state variable that ensures that the robot doesn't go too fast. However, I am using a slider to adjust the maxThrottle command. When you press the W key (forward) you should get the current maxThrottle['forward'] value assigned to the throttleCommand variable. However, every time the handleKey function runs, I just get throttleCommand['forward'] set to the initial value of 30, even if I have changed maxThrottle (using setMaxThrottle) to a higher number like 80.
function App(){
//state hooks
const [ throttleCommand, setThrottleCommand ] = useState({ forward: 0, turn: 0 });
const [ maxThrottle, setMaxThrottle ] = useState({ forward: 30, turn: 15 });
useEffect(
() => {
sendThrottle(throttleCommand);
},
[ throttleCommand ]
);
const handleKey = (e) => {
switch (e.code) {
case 'KeyW':
setThrottleCommand({ ...throttleCommand, forward: maxThrottle.forward });
break;
case 'KeyS':
//THIS SHOULD TURN IT OFFF
setThrottleCommand({ forward: 0, turn: 0 });
break;
case 'KeyA':
//turn left
setThrottleCommand({ ...throttleCommand, turn: -maxThrottle.turn });
break;
case 'KeyD':
setThrottleCommand({ ...throttleCommand, turn: maxThrottle.turn });
break;
default:
break;
}
};
const sendThrottle = () => {
//here I test the throttleCommand
console.log('sending command', throttleCommand);
axios.get(`/throttle/${throttleCommand.forward}/${throttleCommand.turn}`).then((res) => {
setThrottleData(res.data);
});
};
....
}
I have verified that I successfully update maxThrottle to {forward:80,turn:20} but when I press the W key, the throttleCommand is logged as {forward:30, turn:0}. I am expecting to see {forward:80,turn:0} assigned to throttleCommand.
Is there something wrong with using a state variable inside the handleKey function? Why am I always getting the initial value of maxThrottle assigned to throttleCommand?
Michael Bauer! Thanks for the fix. You are 100% correct. Code now works. here is the change:
incorrect keydown listener:
useEffect(() => {
document.addEventListener('keydown',handleKey);
}, []);
SOLVED / CORRECT keydown listener
useEffect(() => {
document.addEventListener('keydown', (e) => {
handleKey(e);
});
}, []);
Lesson learned - use arrow functions inside of useEffect so that the callback function updates!
Related
The if statement in canBookSlot() is only checked once for some reason. The second time canBookSlot() is triggered, the userDetailsObj.canBook should be 0 after running updateUser(). And according to the console log it is the case, but the if statement still runs, why?
let userDetailsString = localStorage.getItem("userDetails");
let userDetailsObj = JSON.parse(userDetailsString);
const updateUser = () => {
userDetailsObj["hasBooked"] = 1;
userDetailsObj["canBook"] = 0;
};
const canBookSlot = (id) => {
if (userDetailsObj.canBook != 0) { // always true
updateUser();
Axios.post("http://localhost:3001/api/book/week1/ex", {
room: userDetailsObj.room,
id: id.id + 1,
}).then(() => updateData());
} else {
console.log("already booked");
}
};
After each render userDetailsObj will take that value from localStorage. That's how every variable inside a component which isn't a state made with useState hook, or a ref made with useRef hook behaves. You can fix your problem this by using a state, like so:
const [userDetails, setUserDetails] = useState(JSON.parse(localStorage.getItem("userDetails")));
const updateUser = () => {
const newUserDetails = { ...userDetailsObj, hasBooked: 1, canBook: 0 };
setUserDetails(newUserDetails);
localStorage.setItem("userDetails", JSON.stringify(newUserDetails));
};
const canBookSlot = (id) => {
if (userDetails.canBook != 0) {
//Always true
updateUser();
Axios.post("http://localhost:3001/api/book/week1/ex", {
room: userDetailsObj.room,
id: id.id + 1,
}).then(() => updateData());
} else {
console.log("already booked");
}
};
Can you clarify where this code runs? Are you using a class component or functional component? Would you mind sharing the entire component? If it is doing what I think it is doing, the let userDetailsString = localStorage.getItem("userDetails"); is running every render which means on every render, it grabs the value in localStorage and uses that, rather than using your object stored in userDetailsObj.
If you are using functional components, you could fix this by using state.
let userDetailsString = localStorage.getItem("userDetails");
let [userDetailsObj, updateUserDetailObj] = useState(JSON.parse(userDetailsString));
const updateUser = () => {
let u = { ...userDetailsObj,
hasBooked: 1,
canBook: 0,
}
updateUserDetailObj(u);
};
If you are using class Components, let me know and I'll update it with that option.
I have a local state selectedProducts holding a number representing the number of products the client has selected, while exiting the screen I want to update the number in redux so it can be viewed in the cart on a different screen.
but every time my local selectedProducts updates it stacks another function call on beforeRemove event
I know this is a noob problem but I've spent hours trying to find a solution to this problem and advice would be very helpful :)
useEffect(()=>{
navigation.addListener('beforeRemove', () => {
console.log('check this', selectedProducts);
if (!selectedProducts) {
return;
}
dispatch(addToCartMultipleQty(selectedProducts));
});
},[selectedProducts])
selectedProducts is a number state whose initial value is null and on a button click event its value is either incremented or decremented by 1, if its previous value is null then its value is set to 1
Note: I just want to update selectedProducts state's latest value only once in redux when the screen is about to be exited/unmonted
You can try this:
useEffect(()=>{
navigation.addListener('beforeRemove', () => {
console.log('check this', selectedProducts);
if (!selectedProducts) {
return;
}
dispatch(addToCartMultipleQty(selectedProducts));
});
return () => {
navigation.removeListener('beforeRemove');
}
}, [selectedProducts])
Add that in return at the end of useEffect it will work as componentWillUnmount in functional component
useEffect(() => {
return () => {
// Anything in here is fired on component unmount.
}
}, [])
Edit: In Your case
useEffect(() => {
console.log('check this', selectedProducts);
if (!selectedProducts) {
return;
}
return () => {
// Anything in here is fired on component unmount.
if (selectedProducts) {
dispatch(addToCartMultipleQty(selectedProducts));
}
};
}, [selectedProducts]);
i am making a game but i am having a problem using setState hook on react, it isnt updating my state, my state properties are
const [state,setState] =useState({
score:0,
holes:9,
initGame:false,
lastHole:-1,
minPeepTime: 200,
maxPeepTime: 1000})
i also have 2 useEffect hook just for testing
useEffect(()=>{
console.log("init game");
console.log(state.initGame);
console.log("--------------");
},[state.initGame])
useEffect(()=>{
console.log("last hole");
console.log(state.lastHole);
console.log("##############");
},[state.lastHole])
and i have a function to start the game
const StartGame=()=>{
console.log("here")
setState({...state,initGame: true,score:0,lastHole: 15})
peep();
setTimeout(() => {
console.log("Game Over")
setState({...state,lastHole:-1,initGame: false})
}, 10000)
}
"here" is actually being printed, and when time is over "Game Over" is being printed.
peep() function update the property lastHole and it call useEffect correctly, but property initGame is never being updated by setState and that my actual problem, useEffect for that property is never call (just the first time when my page is loaded, but not on StartGame function) and its value never change
UPD:
this is the peep function.
const randomTime = (min, max) => {
return Math.round(Math.random() * (max - min) + min);
};
const randomHole = () => {
const idx = Math.floor(Math.random() * state.holes);
if (idx === state.lastHole){
return randomHole();
}
setState({...state,lastHole: idx});
}
const peep = () => {
randomHole();
let time = randomTime(state.minPeepTime,state.maxPeepTime)
if(state.initGame){
setTimeout(() => {
peep()
}, time)
}
}
and this is the output of the code
To properly update state in this scenario use:
setState((initialState) => ({
...initialState,
initGame: true,
score: 0,
lastsHole: 15
}))
The setTimeout part becomes
setState((initialState) => ({
...initialState,
lastHole: -1,
initGame: false
}))
I don't know what peek does or the importance of the setTimeout though;
The problem here is with the setState. setState doesn't immediately change the state as you expect.
setState({
...state,
initGame: true,
score: 0,
lastHole: 15,
});
peep() // value of initGame is false
Unexpectedly, the value of initGame will still be false. Change all the setstates like given below.
setState((state) => ({
...state,
initGame: true,
score: 0,
lastHole: 15,
}));
This will successfully set initGame to true before calling peep()
The final output will be as shown below.
I have this code in my constructor:
this.state = {
tests: [
{
question: "1",
answer: "2",
user: ""
},
{
question: "1",
answer: "2",
user: ""
},
],
};
I have edit function where I read event value in my input:
edit(id, event) {
this.state.tests[id].user = event.target.value;
this.setState({tests:this.state.tests});
}
But es give me this warning:
Do not mutate state directly. Use setState()
react/no-direct-mutation-state
What can i do in this case? Maybe somehow change the line with the assignment event.target.value into setState()?
You can use map() to create copy of tests
edit(id, event) {
const user = event.target.value;
const tests = this.state.tests.map((x,i) => i === id ? {...x, user} : x);
this.setState({tests});
}
One way I tend to go is to make a copy of the array first and then change an item in it, or change the array itself, and then set the state
var tests = this.state.tests.slice(0);
tests[id].user = event.target.value;
this.setState({tests:tests});
You may want to deep-clone the array in some cases, sometimes not.
You are correct, that the problem is with the line:
this.state.tests[id].user = event.target.value;
That's the point where you are mutating your state directly.
You have a few options.
You can "clone" the array first and then update it:
const newTests = [...this.state.tests];
newTests[id].user = event.target.value;
this.setState({tests: newTests});
You could also use immutability-helper:
const {value} = event.target;
this.setState(prevState => update(prevState, {[id]: {user: {$set: value}}}));
In this example, you need to extract value from your event, because it's not safe to access event values in asynchronous calls after an event has been handled.
edit(id, event) {
var newNote = {...this.state.tests[id]}
newNote.user = event.target.value
this.setState({ tests: [...this.state.tests, newNote]})
}
First of all, when you try to set a the new state using the data from previous state you have to use the updater as a function
https://reactjs.org/docs/react-component.html#setstate
const edit = (id, event) => {
this.setState((prevState) => {
const tests = [...prevState.tests];
tests[id] = {
...tests[id],
user: event.target.value
};
return {
...prevState,
tests
};
});
};
When state is not heavy, I use the following codes:
edit (id, event) {
const cp = JSON.parse(JSON.stringify(this.state.tests))
cp[id].user = event.target.value
this.setState({ tests: cp })
}
Update: I found Immer solves it perfectly.
import produce from 'immer'
edit (id, event) {
this.setState(
produce(draft => draft.tests[id].user = event.target.value)
)
}
I am trying to test a React component which uses one of the overloads for setState, but am unsure how to assert the call correctly. An example component would be:
class CounterComponent extends React.Component {
updateCounter() {
this.setState((state) => {
return {
counterValue: state.counterValue + 1
};
});
}
}
The assumption here is that this method will be called asyncronously, so cannot rely on the current state, outwith the call to setState (as it may change before setState executes). Can anyone suggest how you would assert this call? The following test fails as it is simply comparing the function names.
it("Should call setState with the expected parameters", () => {
const component = new CounterComponent();
component.setState = jest.fn(() => {});
component.state = { counterValue: 10 };
component.updateCounter();
const anonymous = (state) => {
return {
counterValue: state.counterValue + 1
};
};
//expect(component.setState).toHaveBeenCalledWith({ counterValue: 11 });
expect(component.setState).toHaveBeenCalledWith(anonymous);
});
Edit: Given yohai's response below, i will add some further context as I feel i may have over simplified the problem however i do not want to re-write the entire question for clarity.
In my actual component, the state value being edited is not a simple number, it is an array of objects with the structure:
{ isSaving: false, hasError: false, errorMessage: ''}
and a few other properties. When the user clicks save, an async action is fired for each item in the array, and then the corresponding entry is updated when that action returns or is rejected. As an example, the save method would look like this:
onSave() {
const { myItems } = this.state;
myItems.forEach(item => {
api.DoStuff(item)
.then(response => this.handleSuccess(response, item))
.catch(error => this.handleError(error, item));
});
}
The handle success and error methods just update the object and call replaceItem:
handleSuccess(response, item) {
const updated = Object.assign({}, item, { hasSaved: true });
this.replaceItem(updated);
}
handleError(error, item) {
const updated = Object.assign({}, item, { hasError: true });
this.replaceItem(updated);
}
And replaceItem then replaces the item in the array:
replaceItem(updatedItem) {
this.setState((state) => {
const { myItems } = state;
const working = [...myItems];
const itemToReplace = working.find(x => x.id == updatedItem.id);
if (itemToReplace) {
working.splice(working.indexOf(itemToReplace), 1, updatedItem);
};
return {
myItems: working
};
});
}
replaceItem is the method I am trying to test, and am trying to validate that it calls setState with the correct overload and a function which correctly updated the state.
My answer below details how I have solved this for myself,but comments and answers are welcome =)
#Vallerii: Testing the resulting state does seem a simpler way, however if i do, there is no way for the test to know that the method is not doing this:
replaceItem(updatedItem) {
const { myItems } = state;
const working = [...myItems];
const itemToReplace = working.find(x => x.id == updatedItem.id);
if (itemToReplace) {
working.splice(working.indexOf(itemToReplace), 1, updatedItem);
};
this.setState({ myItems: working });
}
When replaceItem does not use the correct overload for setState, this code fails when called repeatedly as (I assume) react is batching updates and the state this version uses is stale.
I think you should test something a little bit different and it will look somthing like this (I'm using enzyme):
import React from 'react'
import { mount } from 'enzyme'
import CounterComponent from './CounterComponent'
it("Should increase state by one", () => {
const component = mount(<CounterComponent />)
const counter = 10;
component.setState({ counter });
component.instance().updateCounter();
expect(component.state().counter).toEqual(counter + 1);
});
I have come up with a solution to this after some further thought. I am not sure it is the best solution, but given that the updateCounter method in the example above passes a function into the setState call, I can simply get a reference to that function, execute it with a known state and check the return value is correct.
The resulting test looks like this:
it("Should call setState with the expected parameters", () => {
let updateStateFunction = null;
const component = new CounterComponent();
component.setState = jest.fn((func) => { updateStateFunction = func;});
component.updateCounter();
const originalState = { counterValue: 10 };
const expectedState = { counterValue: 11};
expect(component.setState).toHaveBeenCalled();
expect(updateStateFunction(originalState)).toEqual(expectedState);
});