I need to test the state change after a useEffect
FooView.jsx :
const [total, setTotal] = useState(0);
useEffect(() => {
calculatedTotal = calculateTotal(page.paymentSchedule);
setTotal(calculatedTotal);
}, [
page.paymentSchedule,
]);
FooView-test.jsx :
describe('...', () => {
const paymentSchedule = [
{
count: 2,
amount: 100,
},
{
count: 3,
amount: 200,
}
];
it('should update total when the payment schedule changes', () => {
const container = mount(<FooView />);
container.find('FooView').prop('paymentSchedule')(paymentSchedule);
// what to do next
});
}
I use Jest and Enzyme.
How do I test the resulting state value ?
You need to set another UseEffect like this one:
useEffect(() => {
console.log(total)
}, [total]);
Like that, on each change done to total via setTotal will be returned in your console
Related
In the code snippet below, I add a random number to an array every 3 seconds using setInterval. This goes well, until I try to also call the function on the first render (see the commented line). This gives me this error: Maximum update depth exceeded.
const [listItems, setListItems] = useState([]);
useEffect(() => {
function extendTheList() {
const randNr = Math.floor(Math.random() * 10);
setListItems([...listItems, randNr]);
}
// extendTheList();
const int = setInterval(() => {
extendTheList();
}, 3000);
return () => clearInterval(int);
}, [listItems]);
Sandbox: https://codesandbox.io/s/vigilant-shamir-ltkh6m?file=/src/App.js
Remove the dependency to avoid infinite loop
const [listItems, setListItems] = useState([]);
useEffect(() => {
function extendTheList() {
const randNr = Math.floor(Math.random() * 10);
setListItems(listItems => [...listItems, randNr]);
}
extendTheList();
const int = setInterval(() => {
extendTheList();
}, 3000);
return () => clearInterval(int);
}, []);
https://codesandbox.io/s/goofy-stallman-e1m4wo
You have listItems in the dependency array of useEffect which will retrigger the useEffect every time you change listItems.
If you want to use the old value of the state use the function version of setState
const [listItems, setListItems] = useState([]);
useEffect(() => {
function extendTheList() {
const randNr = Math.floor(Math.random() * 10);
setListItems((currentItems) => [...currentItems, randNr]);
}
// extendTheList();
const int = setInterval(() => {
extendTheList();
}, 3000);
return () => clearInterval(int);
}, [setListItems]);
I took code based off this page and adjusted it. I want to time the amount of milliseconds the user is on the component so I want to log the counter value when the component unmounts aka the return statement of useffect/componentWillUnmount().
const [milliseconds, setMilliseconds] = useState(0);
const isActive = useState(true);
const logger = new logger(stuff);
useEffect(() => {
initializeIcons(undefined, { disableWarnings: true });
});
useEffect(() => {
return () => {
console.log("empty useffect milliseconds:", milliseconds);
logger(milliseconds);
clearInterval(milliseconds)
};
}, []);
useEffect(() => {
let interval: NodeJS.Timeout = setInterval(() => {
}, 0);
interval = setInterval(() => {
setMilliseconds(milliseconds => milliseconds + 1000);
}, 1000);
console.log("interval:", interval);
console.log("interval milliseconds:", milliseconds);
}, [ milliseconds]);
I see the millisecond printout fine in the "interval milliseconds" console statement but the "empty useffect milliseconds:" always prints out 0. What am I doing wrong?
You can remember a mount timestamp and then calculate the difference.
useEffect(() => {
const mountedAt = Date.now();
return () => {
const mountedMilliseconds = Date.now() - mountedAt;
console.log(mountedMilliseconds);
};
}, []);
Side note 1: use an empty array as deps if you want to run function on mount only. If you do not pass [] deps, your initializeIcons effect will run with each re-render. Do it like this:
useEffect(() => {
initializeIcons(undefined, { disableWarnings: true });
}, []);
Side note 2: first interval you create creates a memory leak, because it does nothing, and is never cleared.
Another problem you have is milliseconds dependency in useEffect, which registers new intervals after each milliseconds state change.
I've been trying to convert the following code from React Class Component to Function Component but I've been having problems since I've gotten the error "Expected an assignment or function call and instead saw an expression. eslint no-unused-expressions"
componentDidMount() {
this.startingSequence();
}
startingSequence = () => {
setTimeout(() => {
this.setState(
() => {
return {
textMessageOne: `A wild ${this.state.enemyName} appeared!`,
enemyFaint: false
};
},
() => {
setTimeout(() => {
this.setState(
{
textMessageOne: `Go ${this.state.playerName}!`,
playerFaint: false
},
() => {
setTimeout(() => {
this.setState({
textMessageOne: ""
});
}, 3000);
}
);
}, 3000);
}
);
}, 1000);
};
This is the code I ended up with while trying to convert it to Function Component:
const startingSequence = () => {
setTimeout(() => {
() => {
setTextMessageOne(state => {
state = (`Wild ${enemyName} appeared!`)
return state;})
setEnemyFaint(state => {
state = false
return state;})
}
,
() => {
setTimeout(() => {
setTextMessageOne(`Go ${playerName}!`),
setPlayerFaint(false)
,
() => {
setTimeout(() => {
setTextMessageOne("")
}, 3000);
}
}, 3000);
}
}, 1000);
};
useEffect(() => {
startingSequence();
})
EDIT:
Solution I got thanks to Kieran Osgood:
const startingSequence = () => {
setTimeout(() => {
setTextMessageOne(`Wild ${enemyName} appeared!`)
setEnemyFaint(false)
setTimeout(() => {
setTextMessageOne(`Go ${playerName}!`)
setPlayerFaint(false)
setTimeout(() => {
setTextMessageOne('')
}, 3000)
}, 3000)
}, 1000)
}
useEffect(() => {
startingSequence()
}, [enemyFaint])
In the functional component syntax you can pass the new state in directly OR use the function syntax if you need access to the previous state, however the state variable is not assignable so when you're doing this:
setTextMessageOne(state => {
state = `Wild ${enemyName} appeared!`
return state
})
You could do it simply like this:
setTextMessageOne(`Wild ${enemyName} appeared!`)
Function syntax is helpful for lets say a counter, where we're incrementing a number, and avoids getting stale closures overlapping each other.
setCounter(previousState => {
return previousState + 1
})
// OR
setCounter(previousState => previousState + 1)
So amending that, the other issue is theres a lot of nested arrow functions which seem to stem from the previous usage of the second argument to setState which is a callback to be executed immediately after the state is set - this doesn't exist in functional components, so you should probably refactor this function to be something more along the lines of
// this is just a basic representation, consider combining these to objects etc.
const [enemyName, setEnemyName] = React.useState('')
const [enemyFaint, setEnemyFaint] = React.useState(false)
const [playerFaint, setPlayerFaint] = React.useState(false)
const [textMessageOne, setTextMessageOne] = React.useState('')
const [playerName, setPlayerName] = React.useState('')
const startingSequence = () => {
setTimeout(() => {
setTextMessageOne(state => {
state = `Wild ${enemyName} appeared!`
return state
})
setEnemyFaint(false)
}, 1000)
}
React.useEffect(() => {
setTimeout(() => {
setTextMessageOne(`Go ${playerName}!`)
setPlayerFaint(false)
setTimeout(() => {
setTextMessageOne('')
}, 3000)
}, 3000)
}, [enemyFaint])
Then you want to take these further to extract into custom hooks so its more clear your intent in the flow of your component but generally this is the way in functional components to respond to state changes, via the useEffect
I want to update a state after some other state is updated:
export default function App() {
const [diceNumber, setDiceNumber] = useState(0);
const [rolledValues, setRolledValues] = useState([
{ id: 1, total: 0 },
{ id: 2, total: 0 },
{ id: 3, total: 0 },
{ id: 4, total: 0 },
{ id: 5, total: 0 },
{ id: 6, total: 0 }
]);
const rollDice = async () => {
await startRolingSequence();
};
const startRolingSequence = () => {
return new Promise(resolve => {
for (let i = 0; i < 2500; i++) {
setTimeout(() => {
const num = Math.ceil(Math.random() * 6);
setDiceNumber(num);
}, (i *= 1.1));
}
setTimeout(resolve, 2600);
});
};
useEffect(()=>{
if(!diceNumber) return;
const valueIdx = rolledValues.findIndex(val => val.id === diceNumber);
const newValue = rolledValues[valueIdx];
const {total} = newValue;
newValue.total = total + 1;
setRolledValues([
...rolledValues.slice(0,valueIdx),
newValue,
...rolledValues.slice(valueIdx+1)
])
}, [diceNumber]);
return (
<div className="App">
<button onClick={rollDice}>Roll the dice</button>
<div> Dice Number: {diceNumber ? diceNumber : ''}</div>
</div>
);
}
Here's a sandbox
When the user rolls the dice, a couple setTimeouts will change the state value and resolve eventually. Once it resolves I want to keep track of the score in an array of objects.
So when I write it like this, it works but eslint gives me a warning of a missing dependency. But when I put the dependency in, useEffect will end in a forever loop.
How do I achieve a state update after a state update without causing a forever loop?
Here's a way to set it up that keeps the use effect and doesn't have a dependency issue in the effect: https://codesandbox.io/s/priceless-keldysh-wf8cp?file=/src/App.js
This change in the logic of the setRolledValues call includes getting rid of an accidental mutation to the rolledValues array which could potentially cause issues if you were using it in other places as all React state should be worked with immutably in order to prevent issues.
The setRolledValues has been changed to use the state callback option to prevent a dependency requirement.
useEffect(() => {
if (!diceNumber) return;
setRolledValues(rolledValues => {
const valueIdx = rolledValues.findIndex(val => val.id === diceNumber);
const value = rolledValues[valueIdx];
const { total } = value;
return [
...rolledValues.slice(0, valueIdx),
{ ...value, total: total + 1 },
...rolledValues.slice(valueIdx + 1)
];
});
}, [diceNumber]);
I wouldn't recommend working with it like this, though as it has an issue where if the same number is rolled multiple times in a row, the effect only triggers the first time.
You can move the logic into the rollDice callback instead, which will get rid of both issues that was occurring. https://codesandbox.io/s/stoic-visvesvaraya-402o1?file=/src/App.js
I added a useCallback around rollDice to ensure it doesn't change references so it can be used within useEffects.
const rollDice = useCallback(() => {
const num = Math.ceil(Math.random() * 6);
setDiceNumber(num);
setRolledValues(rolledValues => {
const valueIdx = rolledValues.findIndex(val => val.id === num);
const value = rolledValues[valueIdx];
const { total } = value;
return [
...rolledValues.slice(0, valueIdx),
// newValue,
{ ...value, total: total + 1 },
...rolledValues.slice(valueIdx + 1)
];
});
}, []);
You should be able to just stick all the logic within setRolledValues
useEffect(() => {
if (!diceNumber) return;
setRolledValues((prev) => {
const valueIdx = prev.findIndex(val => val.id === diceNumber);
const newValue = prev[valueIdx];
const { total } = newValue;
newValue.total = total + 1;
return [
...prev.slice(0, valueIdx),
newValue,
...prev.slice(valueIdx + 1)
]
})
}, [diceNumber]);
EDIT: As others have pointed out, useEffect for this application appears to be ill-suited, as you could simply update setRolledValues in your function instead.
If there is some sort of underlying system we're not being shown where you must use an observer pattern like this, you can change the datatype of diceNumber to an object instead, that way subsequent calls to the same number would trigger useEffect
I want to delete some entries from an array after a specified amount of time. notifications should be changed immediately, but in practice it remains unchanged. Is there a way to achieve my goal?
const Notifications = props => {
const [notifications, setNotifications] = useState([
{ value: '123', time: Date.now() },
{ value: '456', time: Date.now() }
]);
useEffect(() => {
let interval = setInterval(() => {
console.log(notifications);
let time = Date.now();
let array = notifications.filter(function(item) {
return time < item.time + 0;
});
setNotifications(array);
}, 500);
return () => {
clearInterval(interval);
};
}, []);
return null
}
Your code is fine! the problem is where you're logging it.
All state updates are asynchronous. With hooks the updated value is exposed in the scope of your component only in the next render call. If you log outside your effect you should see the correct values
const Notifications = props => {
const [notifications, setNotifications] = useState([
{ value: '123', time: Date.now() },
{ value: '456', time: Date.now() }
]);
useEffect(() => {
let interval = setInterval(() => {
let time = Date.now();
let array = notifications.filter(function(item) {
return time < item.time + 0;
});
setNotifications(array);
}, 500);
return () => {
clearInterval(interval);
};
}, []);
return null
}
console.log(notifications);