State no updating in class component when testing method with jest - reactjs

I am trying to test my method in a context provider. I have one branch in the method to be covered and that's what I am strangling with. The specific branch is only entered when a specific condition occurs: if (offset !== 0 && total !== 0 && offset >= total)
See my Class component below:
class JourneyProvider extends Component<
{
children: ReactNode;
},
JourneyContextData
> {
constructor(props: { children: ReactNode }) {
super(props);
this.state = {
...defaultValues,
};
}
getContextValue = (): JourneyContextData => {
const { products, total, limit, offset, loading, disabled, setProducts } =
this.state;
return {
products,
total,
limit,
offset,
loading,
disabled,
setProducts,
};
};
setProducts = async (): Promise<void> => {
const { limit, offset, total, products } = this.state;
if (total === 0 || offset < total) {
const gqlRequest = new GQLRequest(query);
this.setLoading(true);
try {
await gqlRequest.post().then(({ products: { edges, totalCount } }) => {
const newOffset = offset + limit;
this.setState({
products,
total: totalCount,
offset: newOffset,
});
this.setLoading(false);
// Disable button if there are no more products
if (offset !== 0 && total !== 0 && offset >= total) {
// never gets in here when testing.
this.setDisabled(true);
}
});
} catch (e) {
this.setLoading(false);
}
}
};
}
This is my test:
it("setProducts is successful and disable button", async () => {
const wrapper = shallow(
<JourneyProvider>
<div>test</div>
</JourneyProvider>
) as any;
const result = {
products: {
edges: [
{
node: {
id: "1",
name: "test-name",
},
},
],
totalCount: 1,
},
};
mockedClient.post.mockResolvedValueOnce(result);
jest
.spyOn(ProductsQuery, "getProductsQuery")
.mockResolvedValueOnce(new Query("test", true) as never);
const setLoadingSpy = jest.spyOn(wrapper.instance(), "setLoading");
const setDisabledSpy = jest.spyOn(wrapper.instance(), "setDisabled");
wrapper.state().limit = result.products.totalCount;
console.log(wrapper.state().offset); //this returns 0
await wrapper.instance().setProducts();
console.log(wrapper.state().offset); //this returns 0
expect(setLoadingSpy).toHaveBeenCalledWith(true); // this passes
expect(setLoadingSpy).toHaveBeenCalledWith(false); // this passes
expect(setDisabledSpy).toHaveBeenCalledWith(true); // this fails
});

You should change the logic in the code where you're making the comparison because you're using a stale state, keep in mind that a call to setState doesn't change the state immediately. Compare the new values you set to the state instead of the old state values
if (newOffset !== 0 && totalCount !== 0 && newOffset >= totalCount) {
or put that code inside the setState callback to guarantee you're using updated values
...
const newOffset = offset + limit;
this.setState({
products,
total: totalCount,
offset: newOffset,
}, () => {
this.setLoading(false);
if (offset !== 0 && total !== 0 && offset >= total) {
this.setDisabled(true);
}
});
...

Related

Get TypeError: Cannot read properties of undefined (reading 'forEach') when pass a params

At first, the params does not have any data yet (blank array), but it will update again after useEffect set the variable.
But for my highchart, it gave me this error.
TypeError: Cannot read properties of undefined (reading 'forEach')
52 | createChart();
53 | } else {
54 | if (props.allowChartUpdate !== false) {
>55 | if (!props.immutable && chartRef.current) {
| ^ 56 | chartRef.current.update(
57 | props.options,
58 | ...(props.updateArgs || [true, true])
I searched some solutions, they suggest can use allowChartUpdate={false} and immutable={false} for solving the problem. After I tried it, yes it does solve my problem, but my highchart doesn't show the data when initial load.
I'm guessing is it the params passing in a blank array at first, then passing a second time with actual values so it causes this problem. If yes, can rerendering the highchart solve the problem? And how can I do that?
Here is the link, please help me on it. Thank you muakzzz.
You can instead just provide the getRouteData function as an initializer function directly to the useState hook to provide the initial state. So long as it's not asynchronous it will provide initial state for the initial render, no need to use the useEffect hook to populate state after the first render.
Additionally, you should initialize routeMapData to have the data property array by default so you don't accidentally pass it through with an undefined data property, which was part of the problem you were seeing.
export default function App() {
const [routeData] = useState(getRouteData()); // <-- initialize state
const mapStation = () => {
const routeMapData = {
data: [], // <-- data array to push into
};
if (routeData.length !== 0) {
for (let i = 0; i < routeData.length; i++) {
const station = routeData[i].station;
for (let j = 0; j < station.length; j++) {
const firstStation = station[j];
const nextStation = station[j + 1];
if (nextStation) {
routeMapData.data.push([ // <-- push into array
firstStation.stationName,
nextStation.stationName
]);
}
}
}
}
return routeMapData;
};
const content = (key) => {
if (key === "map") {
return <RouteMap mapRouteData={mapStation()} />;
}
return null;
};
return <Box className="rightPaper center">{content("map")}</Box>;
}
You don't need to even use the local state as you can directly consume the returned array from getRouteData in the mapStation utility function.
export default function App() {
const mapStation = () => {
return {
data: getRouteData().flatMap(({ station }) => {
return station.reduce((segments, current, i, stations) => {
if (stations[i + 1]) {
segments.push([
current.stationName,
stations[i + 1].stationName
]);
}
return segments;
}, []);
})
};
};
const content = (key) => {
if (key === "map") {
return <RouteMap mapRouteData={mapStation()} />;
}
return null;
};
return <Box className="rightPaper center">{content("map")}</Box>;
}
Thank you for your help. I managed to get my desired output already. The problem was my parent component will pass a blank array of data into my highchart network graph component at first due to the useEffect used in the parent. And after that, they pass another array with actual data into my highchart network graph component.
import React, {useEffect, useState} from 'react';
import {Box} from "#mui/material";
import RouteMap from "./Content/RouteMap";
import {getRouteData} from "../../../API/RouteDataAPI"
import Timetable from "./Content/Timetable";
import _ from 'lodash';
function RightContent({contentKey}) {
const [routeData, setRouteData] = useState([]);
useEffect(() => {
getRouteData().then(res => setRouteData(res));
}, [])
const mapStation = () => {
let arr = [], allStation = [], routeMapData = {}
if (routeData.length !== 0) {
for (let i = 0; i < routeData.length; i++) {
const station = routeData[i].station;
for (let j = 0; j < station.length; j++) {
const firstStation = station[j];
const nextStation = station[j + 1];
allStation.push(firstStation.stationName)
if (nextStation) {
arr.push([firstStation.stationName, nextStation.stationName])
}
}
}
routeMapData.data = arr;
routeMapData.allStation = allStation;
routeMapData.centralStation = "KL Sentral"
}
return routeMapData;
}
// const mapStation = () => {
// let arr = [];
// getRouteData().then(res => {
// arr.push(res.flatMap(({station}) => {
// return station.reduce((segments, current, i, stations) => {
// if (stations[i + 1]) {
// segments.push(
// current.stationName,
// );
// }
// return segments;
// }, []);
// }))
// })
// console.log(arr)
// }
const content = (key) => {
const availableRoute = routeData.map(route => route.routeTitle);
if (
key === 'map'
// && !_.isEmpty(mapStation())
){
// console.log('here', mapRouteData)
return <RouteMap mapRouteData={mapStation()}/>;
}
else if (availableRoute.includes(key)) {
return <Timetable routeData={routeData} currentRoute={key}/>
} else {
return null;
}
}
return (
<Box className="rightPaper center">
{content(contentKey)}
</Box>
);
}
export default RightContent;
^^^This was my parent component.^^^
In the content variable function there, I have an if statement with the requirements provided. If you try to uncomment the lodash (second requirement) in the if statement, I can able to get my desire result.
This was my highchart network component.
import React, {useEffect, useRef, useState} from 'react'
import Highcharts from 'highcharts/highstock'
import HighchartsReact from 'highcharts-react-official'
import networkgraph from 'highcharts/modules/networkgraph'
require('highcharts/modules/exporting')(Highcharts);
require('highcharts/modules/export-data')(Highcharts);
if (typeof Highcharts === "object") {
networkgraph(Highcharts);
}
const RouteMap = ({mapRouteData}) => {
const [seriesData, setSeriesData] = useState(mapRouteData.data);
const [centralStation, setCentralStation] = useState(mapRouteData.centralStation);
const [allStation, setAllStation] = useState(mapRouteData.allStation);
useEffect(() => {
setSeriesData(mapRouteData.data);
setCentralStation(mapRouteData.centralStation);
setAllStation(mapRouteData.allStation);
}, [mapRouteData])
Highcharts.addEvent(
Highcharts.Series,
'afterSetOptions',
function (e) {
let colors = Highcharts.getOptions().colors,
i = 0,
nodes = {};
if (
this instanceof Highcharts.seriesTypes.networkgraph &&
e.options.id === 'lang-tree' &&
e.options.data !== undefined
) {
let lastSecond = '', arry = []
e.options.data.forEach(function (link) {
if (lastSecond !== link[0]) {
nodes[link[0]] = {
id: link[0],
color: colors[++i]
}
} else if (lastSecond === link[0]) {
nodes[link[0]] = {
id: link[0],
color: colors[i]
}
nodes[link[1]] = {
id: link[1],
color: colors[i]
}
arry.push(link[0])
}
lastSecond = link[1];
});
const exchangeStation = allStation.filter((item, index) => allStation.indexOf(item) !== index);
i += 1;
exchangeStation.forEach((station) => {
nodes[station] = {
id: station,
marker: {
radius: 18
},
name: 'Interchange: ' + station,
color: colors[i]
}
})
nodes[centralStation] = {
id: centralStation,
name: 'Sentral Station: ' + centralStation,
marker: {
radius: 25
},
color: colors[++i]
}
e.options.nodes = Object.keys(nodes).map(function (id) {
return nodes[id];
});
}
}
);
const options = {
chart: {
type: 'networkgraph',
},
title: {
text: 'The Route Map'
},
caption: {
text: "Click the button at top right for more options."
},
credits: {
enabled: false
},
plotOptions: {
networkgraph: {
keys: ['from', 'to'],
layoutAlgorithm: {
enableSimulation: true,
// linkLength: 7
}
}
},
series: [
{
link: {
width: 4,
},
marker: {
radius: 10
},
dataLabels: {
enabled: true,
linkFormat: "",
allowOverlap: false
},
id: "lang-tree",
data: seriesData
}
]
};
return <HighchartsReact
ref={useRef()}
containerProps={{style: {height: "100%", width: "100%"}}}
highcharts={Highcharts}
options={options}
/>;
}
export default RouteMap;
Sorry for the lengthy code here. By the way, feel free to let me know any improvements I can make in my code. First touch on react js project and still have a long journey to go.
Once again~ Thank you!
I fixed adding True for allowChartUpdate and immutable
<HighchartsReact
ref={chartRef}
highcharts={Highcharts}
options={options}
containerProps={containerProps}
allowChartUpdate={true}
immutable={true}
/>

How to update React setState immediately

I'm calling AddItemToList() on "onClick" event.
However, since setTotalExpensesAmount() is asynchronous, it's called too late and axios.post() sends incorrect value of totalExpenses to the database (previous one).
I believe, using useEffect() outside AddItemToList() function is not a correct solution.
Should I make axios.post() the callback of setTotalExpensesAmount()? If so, how can I do it?
const AddItemToList = () => {
if (Expense !== '' && Amount !== '' && Number(Amount) > 0) {
setExpenseAndAmountList(
[
...expenseAndAmountList,
{
expenseTitle: Expense,
expenseAmount: Amount,
id: Math.random() * 1000
}
]
);
axios.post('http://localhost:4000/app/insertedExpenseAndAmount',
{
expenseTitle: Expense,
expenseAmount: Amount
});
setTotalExpensesAmount((currentTotalExpenses: number) => currentTotalExpenses + Number(Amount));
axios.post('http://localhost:4000/app/totalexpensesamount',
{
totalExpensesAmount: totalExpenses
});
setExpense("");
setAmount("");
setIfNotValidInputs(false);
setTotalBalance(Number(income) - totalExpenses);
} else {
setIfNotValidInputs(true);
}
}
There are 2 ways of doing this.
You can calculate the newTotalExpense and do setTotalExpense with the new value. Also, pass the newTotalExpense in the post call you are making.
const AddItemToList = () => {
if (Expense !== '' && Amount !== '' && Number(Amount) > 0) {
setExpenseAndAmountList(
[
...expenseAndAmountList,
{
expenseTitle: Expense,
expenseAmount: Amount,
id: Math.random() * 1000
}
]
);
axios.post('http://localhost:4000/app/insertedExpenseAndAmount',
{
expenseTitle: Expense,
expenseAmount: Amount
});
const newTotalExpense = currentTotalExpenses + Number(Amount)
setTotalExpensesAmount(newTotalExpense);
axios.post('http://localhost:4000/app/totalexpensesamount',
{
totalExpensesAmount: newTotalExpense
});
setExpense("");
setAmount("");
setIfNotValidInputs(false);
setTotalBalance(Number(income) - totalExpenses);
} else {
setIfNotValidInputs(true);
}
}
You can use useEffect hook which will trigger on the change of totalExpense value. You can make the post call inside the useEffect.
useEffect(() => {
axios.post('http://localhost:4000/app/totalexpensesamount',
{
totalExpensesAmount: totalExpenses
});
}, [totalExpense])
const AddItemToList = () => {
if (Expense !== '' && Amount !== '' && Number(Amount) > 0) {
setExpenseAndAmountList(
[
...expenseAndAmountList,
{
expenseTitle: Expense,
expenseAmount: Amount,
id: Math.random() * 1000
}
]
);
axios.post('http://localhost:4000/app/insertedExpenseAndAmount',
{
expenseTitle: Expense,
expenseAmount: Amount
});
setTotalExpensesAmount((currentTotalExpenses: number) => currentTotalExpenses + Number(Amount));
setExpense("");
setAmount("");
setIfNotValidInputs(false);
setTotalBalance(Number(income) - totalExpenses);
} else {
setIfNotValidInputs(true);
}
}

What is the best method to change React State based off of State?

I am in the process of building a React app and I want to be able to set the state of upperScoreBonus as soon as all of the other scores are not undefined. If the total of the all the scores are greater than 63, then add the bonus, otherwise only score 0.
Where I'm stuck is the applyUpperScoreBonus function is delayed until the next time that the roll function is called. I'm at a loss on where I should make this call to applyUpperScoreBonus.
I know I'm missing something.
class Game extends Component {
constructor(props) {
super(props);
this.state = {
dice: Array.from({ length: NUM_DICE }),
locked: Array(NUM_DICE).fill(false),
rollsLeft: NUM_ROLLS,
isRolling: false,
scores: {
ones: undefined,
twos: undefined,
threes: undefined,
fours: undefined,
fives: undefined,
sixes: undefined,
upperBonusScore: undefined
}
};
this.roll = this.roll.bind(this);
this.doScore = this.doScore.bind(this);
this.applyUpperScoreBonus = this.applyUpperScoreBonus.bind(this);
}
doScore(rulename, ruleFn) {
// evaluate this ruleFn with the dice and score this rulename
// only allows an update to the score card if the vaule has not yet been set.
if (this.state.scores[rulename] === undefined) {
this.setState(st => ({
scores: { ...st.scores, [rulename]: ruleFn(this.state.dice)},
rollsLeft: NUM_ROLLS,
locked: Array(NUM_DICE).fill(false)
}));
this.applyUpperScoreBonus();
this.roll();
}
}
applyUpperScoreBonus() {
const st = this.state.scores;
const upperArrayScores = [st.ones, st.twos, st.threes, st.fours, st.fives, st.sixes];
let totalUpperScore = 0;
upperArrayScores.forEach(idx => {
if(idx !== undefined) {
totalUpperScore += idx
}
})
if(upperArrayScores.every(idx => idx !== undefined)) {
//if the total is more than 63, apply bonus of 35 otherwise 0
this.setState(st => ({
scores: { ...st.scores, upperBonusScore: totalUpperScore >= 63 ? 35 : 0},
}));
}
}
Here this.applyUpperScoreBonus() is called after the setState, since the setState({}) is async, this.applyUpperScoreBonus() only get state update on next doScore() call
This is your code block
if (this.state.scores[rulename] === undefined) {
this.setState(st => ({
scores: { ...st.scores, [rulename]: ruleFn(this.state.dice)},
rollsLeft: NUM_ROLLS,
locked: Array(NUM_DICE).fill(false)
}));
this.applyUpperScoreBonus();
this.roll();
}
Change it to
if (this.state.scores[rulename] === undefined) {
this.setState(st => ({
scores: { ...st.scores, [rulename]: ruleFn(this.state.dice)},
rollsLeft: NUM_ROLLS,
locked: Array(NUM_DICE).fill(false)
}),()=> this.applyUpperScoreBonus()); // update here
this.roll();
}
Here, this.applyUpperScoreBonus() is called on setState() callback, so the function will get the updated state value.

Why is not entering componentDidUpdate()?

Hello I'm trying to test a state that is changed in the componentDidUpdate but is not enetering.
Code
componentDidUpdate (newProps) {
const { dataSource } = newProps
// set value for nextButtonDisabled in first results async load
if (dataSource.length) {
const newPaginationInfo = Object.assign({}, this.state.paginationInfo)
newPaginationInfo.nextButtonDisabled = dataSource.length <= this.pageSize
this.setState({ paginationInfo: newPaginationInfo }) /* eslint-disable-line react/no-did-update-set-state */
}
}
State
this.state = {
paginationInfo: {
currentPage: 0,
nextButtonDisabled: true
}
}
And the test
it('should set nextButtonDisabled to false when gets new props.datasource if datasource length <= 20', () => {
const component = shallow(<VehicleHistoryTable {...makeProps()} />)
component.setProps({ dataSource: createDataSourceMock(3) })
expect(component.instance().state.paginationInfo.nextButtonDisabled).toEqual(true)
})
The function createDataSourceMock() creates an array of numbers, in this case 3 rows.
Any suggestions?
P:S I'm trying to migrate to React 17

ReactJS seems combine two state updates as one render, how to see separate rendering effects?

I am trying to come up a react exercise for the flip-match cards game: say 12 pairs of cards hide (face down) randomly in a 4x6 matrix, player click one-by-one to reveal the cards, when 2 cards clicked are match then the pair is found, other wise hide both again., gane over when all pairs are found.
let stored = Array(I * J).fill(null).map((e, i) => (i + 1) % (I * J));
/* and: randomize (I * J / 2) pairs position in stored */
class Board extends React.Component {
constructor() {
super();
this.state = {
cards: Array(I*J).fill(null),
nClicked: 0,
preClicked: null,
clicked: null,
};
}
handleClick(i) {
if (!this.state.cards[i]) {
this.setState((prevState) => {
const upCards = prevState.cards.slice();
upCards[i] = stored[i];
return {
cards: upCards,
nClicked: prevState.nClicked + 1,
preClicked: prevState.clicked,
clicked: i,
};
}, this.resetState);
}
}
resetState() {
const preClicked = this.state.preClicked;
const clicked = this.state.clicked;
const isEven = (this.state.nClicked-1) % 2;
const matched = (stored[preClicked] === stored[clicked]);
if (isEven && preClicked && clicked && matched) {
// this.forceUpdate(); /* no effects */
this.setState((prevState) => {
const upCards = prevState.cards.slice();
upCards[preClicked] = null;
upCards[clicked] = null;
return {
cards: upCards,
nClicked: prevState.nClicked,
preClicked: null,
clicked: null,
};
});
}
}
renderCard(i) {
return <Card key={i.toString()} value={this.state.cards[i]} onClick={() => this.handleClick(i)} />;
}
render() {
const status = 'Cards: '+ I + ' x ' + J +', # of clicked: ' + this.state.nClicked;
const cardArray = Array(I).fill(null).map(x => Array(J).fill(null));
return (
<div>
<div className="status">{status}</div>
{ cardArray.map((element_i, index_i) => (
<div key={'row'+index_i.toString()} className="board-row">
{ element_i.map((element_j, index_j) => this.renderCard(index_i*J+index_j))
}
</div>
))
}
</div>
);
}
}
Essentially, Board constructor initialize the state, and handleClick() calls setState() to update the state so it trigger the render of the clicked card's value; the callback function resetState() is that if the revealed two card did not match, then another setState() to hide both.
The problem is, the 2nd clicked card value did not show before it goes to hide. Is this due to React combine the 2 setState renderings in one, or is it rendering so fast that we can not see the first rendering effects before the card goes hide? How to solve this problem?
You're passing resetState as the callback to setState, so I would expect after the initial click your state will be reset.
You might want to simplify a bit and do something like this:
const CARDS = [
{ index: 0, name: 'Card One', matchId: 'match1' },
{ index: 1, name: 'Card Two', matchId: 'match2' },
{ index: 2, name: 'Card Three', matchId: 'match1', },
{ index: 3, name: 'Card Four', 'matchId': 'match2' },
];
class BoardSim extends React.Component {
constructor(props) {
super(props);
this.state = {
cardsInPlay: CARDS,
selectedCards: [],
checkMatch: false,
updateCards: false
};
...
}
...
componentDidUpdate(prevProps, prevState) {
if (!prevState.checkMatch && this.state.checkMatch) {
this.checkMatch();
}
if (!prevState.updateCards && this.state.updateCards) {
setTimeout(() => {
this.mounted && this.updateCards();
}, 1000);
}
}
handleCardClick(card) {
if (this.state.checkMatch) {
return;
}
if (this.state.selectedCards.length === 1) {
this.setState({ checkMatch: true });
}
this.setState({
selectedCards: this.state.selectedCards.concat([card])
});
}
checkMatch() {
if (this.selectedCardsMatch()) {
...
}
else {
...
}
setTimeout(() => {
this.mounted && this.setState({ updateCards: true });
}, 2000);
}
selectedCardsMatch() {
return this.state.selectedCards[0].matchId ===
this.state.selectedCards[1].matchId;
}
updateCards() {
let cardsInPlay = this.state.cardsInPlay;
let [ card1, card2 ] = this.state.selectedCards;
if (this.selectedCardsMatch()) {
cardsInPlay = cardsInPlay.filter((card) => {
return card.id !== card1.id && card.id !== card2.id;
});
}
this.setState({
selectedCards: [],
cardsInPlay,
updateCards: false,
checkMatch: false
});
}
render() {
return (
<div>
{this.renderCards()}
</div>
);
}
renderCards() {
return this.state.cardsInPlay.map((card) => {
return (
<div key={card.name} onClick={() => this.handleCardClick(card)}>
{card.name}
</div>
);
});
}
...
}
I've created a fiddle for this you can check out here: https://jsfiddle.net/andrewgrewell/69z2wepo/82425/

Resources