Data-testid = 'suggestion' is not being detected while running testcases - javascript

I have prepared a wikipedia search react application which uses api to get the data .
Below here is the UI for the application .
Here is the code of the "Search" component
import React, { useState } from 'react';
import axios from 'axios';
function Search() {
const [searchTerm, setSearchTerm] = useState('Programming');
const [suggestions, setSuggestions] = useState([]);
let timeoutId = null;
const handleSearch = (e) => {
const searchValue = e.target.value;
setSearchTerm(searchValue);
clearTimeout(timeoutId);
if (searchValue.length === 0) {
setTimeout(() => setSuggestions([]), 200);
} else {
timeoutId = setTimeout(() => {
axios.get(`https://en.wikipedia.org/w/api.php?action=opensearch&origin=*&search=${searchValue}`)
.then(res => {
const suggestionsData = res.data[1];
const suggestionsLinks = res.data[3];
const suggestionsList = suggestionsData.map((text, index) => {
return {
text,
link: suggestionsLinks[index]
};
});
setSuggestions(suggestionsList);
})
.catch(err => console.log(err));
}, 500);
}
};
return (
<div className='search'>
<h1 align='center'> Wiki Search</h1>
<input
data-testid="searchterm"
value={searchTerm}
onChange={handleSearch}
/>
{suggestions.map((suggestion, index) => (
<div data-testid="suggestion" style={{backgroundColor:index%2?'#e2c3e7':'#e9d6ec'}}>
<a
key={index}
href={suggestion.link}
>
{suggestion.text} <br></br>
</a>
</div>
))}
</div>
);
}
export default Search;
import { fireEvent, render, screen, waitFor, cleanup, queryByTestId, getByTestId, wait } from "#testing-library/react";
import Search from "./components/Search/Search";
import axios from 'axios';
import fetchData from "./components/Search/utillity";
jest.mock('axios');
console.log(axios);
describe('Search Component', () => {
afterEach(cleanup);
test('testcase1', async () => {
const dataList = {
data : [
'R',
['R', 'Ring', 'Revengers', 'Robusted'],
['', '', '', '', ''],
['www.google.com', 'www.yahoo.com', 'www.duckduckgo.com', 'www.brave.com']
]
}
const { getAllByTestId, queryAllByTestId } = render(<Search />);
axios.get.mockImplementationOnce(() => Promise.resolve(dataList));
let flag = true;
const searchname = ['R', 'Ring', 'Revengers', 'Robusted'];
const searchLinks = ['www.google.com', 'www.yahoo.com', 'www.duckduckgo.com', 'www.brave.com']
console.log("Log ===", searchLinks);
const suggestionList = await screen.findAllByTestId('suggestion');
await suggestionList.forEach((suggestion, index) => {
// console.log(`test = ${suggestion.textContent} exp = ${searchname[index]} at ind = ${index}`);
// console.log(`test = ${suggestion.getAttribute('href')} exp = ${searchLinks[index]} at ind = ${index}`);
expect(suggestion.textContent).toBe(searchname[index]);
expect(suggestion.getAttribute('href')).toBe(searchLinks[index])
})
})
test('testcase2', async () => {
const dataList = {
data : [
'R',
['R', 'Ring', 'Revengers', 'Robusted', 'Revamped'],
['', '', '', '', '', ''],
['www.google.com', 'www.yahoo.com', 'www.duckduckgo.com', 'www.brave.com', 'www.lickhack.go']
]
}
const { getAllByTestId, queryAllByTestId } = render(<Search />);
axios.get.mockImplementationOnce(() => Promise.resolve(dataList));
let searchname = [...dataList.data[1]];
let searchLinks = [...dataList.data[3]];
let suggestionList = await screen.findAllByTestId('suggestion');
await suggestionList.forEach((suggestion, index) => {
// console.log(`test = ${suggestion.textContent} exp = ${searchname[index]} at ind = ${index}`);
// console.log(`test = ${suggestion.getAttribute('href')} exp = ${searchLinks[index]} at ind = ${index}`);
expect(suggestion.textContent).toBe(searchname[index]);
expect(suggestion.getAttribute('href')).toBe(searchLinks[index])
})
});
test('testcase3', async () => {
const dataList = {
data : [
'R',
['R', 'Ring', 'Revengers', 'Robusted', 'Revamped'],
['', '', '', '', '', ''],
['www.google.com', 'www.yahoo.com', 'www.duckduckgo.com', 'www.brave.com', 'www.lickhack.go']
]
}
render(<Search />);
axios.get.mockImplementationOnce(() => Promise.resolve(dataList));
let searchname = [...dataList.data[1]];
let searchLinks = [...dataList.data[3]];
let suggestionList = await screen.findAllByTestId('suggestion');
let input = screen.getByTestId('searchterm');
await fireEvent.change(input, {target : {value : ''}});
await new Promise((r) => setTimeout(r, 300));
await waitFor(() => {
expect(screen.queryAllByTestId('suggestion')).toEqual([]);
})
})
})
While it was working fine when the application is running , not when it is tested against these testcases .
Here is the error message that gets displayed after testcase fails
FAIL src/App.test.js
Search Component
× testcase1 (1061 ms)
× testcase2 (1015 ms)
× testcase3 (1021 ms)
● Search Component › testcase1
Unable to find an element by: [data-testid="suggestion"]
Ignored nodes: comments, script, style
<body>
<div>
<div
class="search"
>
<h1
align="center"
>
Wiki Search
</h1>
<input
data-testid="searchterm"
value="Programming"
/>
</div>
</div>
</body>
33 | /* eslint-disable */;(oo_oo(),console.log("Log ===", searchLinks, `3708101a_1`));
34 |
> 35 | const suggestionList = await screen.findAllByTestId('suggestion');
| ^
36 |
37 | await suggestionList.forEach((suggestion, index) => {
38 |
at waitForWrapper (node_modules/#testing-library/dom/dist/wait-for.js:166:27)
at findAllByTestId (node_modules/#testing-library/dom/dist/query-helpers.js:86:33)
at Object.<anonymous> (src/App.test.js:35:45)
● Search Component › testcase2
Unable to find an element by: [data-testid="suggestion"]
Ignored nodes: comments, script, style
<body>
<div>
<div
class="search"
>
<h1
align="center"
>
Wiki Search
</h1>
<input
data-testid="searchterm"
value="Programming"
/>
</div>
</div>
</body>
65 | let searchLinks = [...dataList.data[3]];
66 |
> 67 | let suggestionList = await screen.findAllByTestId('suggestion');
| ^
68 |
69 | await suggestionList.forEach((suggestion, index) => {
70 |
at waitForWrapper (node_modules/#testing-library/dom/dist/wait-for.js:166:27)
at findAllByTestId (node_modules/#testing-library/dom/dist/query-helpers.js:86:33)
at Object.<anonymous> (src/App.test.js:67:43)
● Search Component › testcase3
Unable to find an element by: [data-testid="suggestion"]
Ignored nodes: comments, script, style
<body>
<div>
<div
class="search"
>
<h1
align="center"
>
Wiki Search
</h1>
<input
data-testid="searchterm"
value="Programming"
/>
</div>
</div>
</body>
97 | let searchLinks = [...dataList.data[3]];
98 |
> 99 | let suggestionList = await screen.findAllByTestId('suggestion');
| ^
100 |
101 | let input = screen.getByTestId('searchterm');
102 |
at waitForWrapper (node_modules/#testing-library/dom/dist/wait-for.js:166:27)
at findAllByTestId (node_modules/#testing-library/dom/dist/query-helpers.js:86:33)
at Object.<anonymous> (src/App.test.js:99:43)
Can someone tell what should i do so the application passed through testcases . Please help me out.

Related

useState defaults appear to rerun after running function on state change, defaults shouldnt run twice

I have the following issue with website where the settings state resets after running more searches. The settings component is show below in the picture, it usually works but if you uncheck a box and then run a few more searches at some point the showAllDividends setting will be set to false, the All dividends component won't be on the screen, but for some reason the checkbox itself is checked (true). This is my first time really working with checkboxes in React, and I think I'm using the onChange feature wrong. Right now I get the event.target.checked boolean, but only onChange.
If that isn't the issue then the most likely cause is the default statements being run again on another render:
const [showMainInfo, setShowMainInfo] = useState(true);
const [showYieldChange, setShowYieldChange] = useState(true);
const [showAllDividends, setShowAllDividends] = useState(true);
the thing is I don't see why the default statements would run more than once, the component isn't being destroyed there's no react router usage. I expected it to keep its current state after the page is first loaded. I think the settings defaults are being rerun, but just don't understand why they would.
I unchecked, checked, unchecked the 'All dividends' checkbox, and it was unchecked when I ran 2 more searches. After the second search the checkbox was checked but the component was gone, because showAllDividends was false
main component SearchPage.js:
import React, {useState, useEffect} from 'react';
import { connect } from 'react-redux';
import axios from 'axios';
import SearchBar from './SearchBar';
import AllDividendsDisplay from './dividend_results_display/AllDividendsDisplay';
import DividendResultsDisplay from './dividend_results_display/DividendResultsDisplay';
import SettingsView from './settings/SettingsView';
const HOST = process.env.REACT_APP_HOSTNAME
const PROTOCOL = process.env.REACT_APP_PROTOCOL
const PORT = process.env.REACT_APP_PORT
const BASE_URL = PROTOCOL + '://' + HOST + ':' + PORT
const SearchPage = ({userId}) => {
const DEFAULT_STOCK = 'ibm';
const [term, setTerm] = useState(DEFAULT_STOCK);
const [debouncedTerm, setDebouncedTerm] = useState(DEFAULT_STOCK);
const [loading, setLoading] = useState(false);
const [recentSearches, setRecentSearches] = useState([DEFAULT_STOCK]);
const [dividendsYearsBack, setDividendsYearsBack] = useState('3');
const [debouncedDividendYearsBack, setDebouncedDividendYearsBack] = useState('3');
const [errorMessage, setErrorMessage] = useState('');
const [dividendsData, setDividendsData] = useState(
{
current_price: '',
recent_dividend_rate: '',
current_yield: '',
dividend_change_1_year: '',
dividend_change_3_year: '',
dividend_change_5_year: '',
dividend_change_10_year: '',
all_dividends: [],
name: '',
description: '',
}
)
const [settingsViewVisible, setSettingsViewVisible] = useState(false);
const [showMainInfo, setShowMainInfo] = useState(true);
const [showYieldChange, setShowYieldChange] = useState(true);
const [showAllDividends, setShowAllDividends] = useState(true);
const onTermUpdate = (term) => {
setTerm(term)
}
// TODO: write a custom hook that debounces taking the term and the set debounced term callback
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedTerm(term);
}, 1500);
return () => {
clearTimeout(timerId);
};
}, [term]);
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedDividendYearsBack(dividendsYearsBack);
}, 1500);
return () => {
clearTimeout(timerId);
};
}, [dividendsYearsBack]);
useEffect(() => {runSearch()}, [debouncedTerm]);
useEffect(() => {
// alert(dividendsYearsBack)
if (dividendsYearsBack !== '') {
runSearch();
}
}, [debouncedDividendYearsBack])
useEffect(() => {
console.log("user id changed")
if (userId) {
const user_profile_api_url = BASE_URL + '/users/' + userId
axios.get(user_profile_api_url, {})
.then(response => {
const recent_searches_response = response.data.searches;
const new_recent_searches = [];
recent_searches_response.map(dict => {
new_recent_searches.push(dict.search_term)
})
setRecentSearches(new_recent_searches);
})
.catch((error) => {
console.log("error in getting user profile: ", error.message)
})
}
}, [userId])
useEffect(() => {
const user_profile_api_url = BASE_URL + '/users/' + userId
const request_data = {searches: recentSearches}
axios.post(user_profile_api_url, request_data)
// .then(response => {
// console.log(response)
// })
}, [recentSearches])
const makeSearchApiRequest = () => {
let dividends_api_url = BASE_URL + '/dividends/' + term + '/' + dividendsYearsBack
if (!recentSearches.includes(term)) {
setRecentSearches([...recentSearches, term])
}
axios.get(dividends_api_url, {})
.then(response => {
// console.log(response)
setLoading(false);
setDividendsData(response.data);
})
.catch((error) => {
console.log(error.message);
setLoading(false);
setErrorMessage(error.message);
})
}
const runSearch = () => {
console.log("running search: ", term);
setErrorMessage('');
if (term) {
setLoading(true);
if (!dividendsYearsBack) {
setDividendsYearsBack('3', () => {
makeSearchApiRequest()
});
} else {
makeSearchApiRequest()
}
}
}
const recentSearchOnClick = (term) => {
setTerm(term);
setDebouncedTerm(term);
}
const removeRecentSearchOnClick = (term) => {
const searchesWithoutThisOne = recentSearches.filter(search => search !== term)
setRecentSearches(searchesWithoutThisOne);
}
const dividendsYearsBackOnChange = (text) => {
setDividendsYearsBack(text);
}
const renderMainContent = () => {
if (!debouncedTerm) {
return (
<div className="ui active">
<div className="ui text">Search for info about a stock</div>
</div>
)
}
if (loading === true) {
return (
<div className="ui active dimmer">
<div className="ui big text loader">Loading</div>
</div>
)
}
if (errorMessage) {
return (
<div className="ui active">
<div className="ui text">{errorMessage}</div>
</div>
)
} else {
return (
<DividendResultsDisplay
data={dividendsData}
dividends_years_back={dividendsYearsBack}
dividendsYearsBackOnChange={dividendsYearsBackOnChange}
showMainInfo={showMainInfo}
showYieldChange={showYieldChange}
showAllDividends={showAllDividends}/>
)
}
}
// https://stackoverflow.com/questions/38619981/how-can-i-prevent-event-bubbling-in-nested-react-components-on-click
const renderRecentSearches = () => {
return recentSearches.map((term) => {
return (
<div key={term}>
<button
onClick={() => recentSearchOnClick(term)}
style={{marginRight: '10px'}}
>
<div>{term} </div>
</button>
<button
onClick={(event) => {event.stopPropagation(); removeRecentSearchOnClick(term)}}>
X
</button>
<br/><br/>
</div>
)
})
}
const renderSettingsView = (data) => {
if (settingsViewVisible) {
return (
<SettingsView data={data} />
)
} else {
return null;
}
}
const toggleSettingsView = () => {
setSettingsViewVisible(!settingsViewVisible);
}
const toggleDisplay = (e, setter) => {
setter(e.target.checked)
}
const SETTINGS_DATA = [
{
label: 'Main info',
id: 'main_info',
toggler: toggleDisplay,
setter: setShowMainInfo
},
{
label: 'Yield change',
id: 'yield_change',
toggler: toggleDisplay,
setter: setShowYieldChange
},
{
label: 'Dividends list',
id: 'all_dividends',
toggler: toggleDisplay,
setter: setShowAllDividends
},
]
console.log("showMainInfo: ", showMainInfo);
console.log("showYieldChange: ", showYieldChange);
console.log("showAllDividends: ", showAllDividends);
return (
<div className="ui container" style={{marginTop: '10px'}}>
<SearchBar term={term} onTermUpdate={onTermUpdate} />
{renderRecentSearches()}
<br/><br/>
<button onClick={toggleSettingsView}>Display settings</button>
{renderSettingsView(SETTINGS_DATA)}
<div className="ui segment">
{renderMainContent()}
</div>
</div>
)
}
const mapStateToProps = state => {
return { userId: state.auth.userId };
};
export default connect(
mapStateToProps
)(SearchPage);
// export default SearchPage;
the settingsView component:
import React from 'react';
import SettingsCheckbox from './SettingsCheckbox';
const SettingsView = ({data}) => {
const checkboxes = data.map((checkbox_info) => {
return (
<div key={checkbox_info.id}>
<SettingsCheckbox
id={checkbox_info.id}
label={checkbox_info.label}
toggler={checkbox_info.toggler}
setter={checkbox_info.setter}/>
<br/>
</div>
)
});
return (
<div className="ui segment">
{checkboxes}
</div>
);
}
export default SettingsView;
SettingsCheckbox.js:
import React, {useState} from 'react';
const SettingsCheckbox = ({id, label, toggler, setter}) => {
const [checked, setChecked] = useState(true)
return (
<div style={{width: '200px'}}>
<input
type="checkbox"
checked={checked}
id={id}
name={id}
value={id}
onChange={(e) => {
setChecked(!checked);
toggler(e, setter);
}} />
<label htmlFor="main_info">{label}</label><br/>
</div>
);
}
export default SettingsCheckbox;

Assign a value by default instead of using v-model to get user input

Im new into VueJS.
I'm trying to change this piece of code,
At the moment "farm" is get by the model -> v-model="farm"
I would like to hide the input text and put farm as a const, so its always the same value.
I never did vueJS so I will appreciate any kind of tips to achieve this.
Here the demo : https://www.gemfarm.gg/farmer
Here all the code :
<template>
<ConfigPane />
<div v-if="!wallet" class="text-center">We advise you to use a burner wallet</div>
<div v-else>
<!--farm address-->
<div class="nes-container with-title mb-10">
<p class="title">Connect to a Farm</p>
<div class="nes-field mb-5">
<label for="farm">Farm address:</label>
<input id="farm" class="nes-input" v-model="farm" />
</div>
</div>
<div v-if="farmerAcc">
<FarmerDisplay
:key="farmerAcc"
:farm="farm" // THIS HAS TO BE A STATIC ID LIKE "0E821U932EUN923UE9N3Y8"
:farmAcc="farmAcc"
:farmer="farmer"
:farmerAcc="farmerAcc"
class="mb-10"
#refresh-farmer="handleRefreshFarmer"
/>
<Vault
:key="farmerAcc"
class="mb-10"
:vault="farmerAcc.vault.toBase58()"
#selected-wallet-nft="handleNewSelectedNFT"
>
<button
v-if="farmerState === 'staked' && selectedNFTs.length > 0"
class="nes-btn is-primary mr-5"
#click="addGems"
>
Add Mekamounts (resets staking)
</button>
<button
v-if="farmerState === 'unstaked'"
class="nes-btn is-success mr-5"
#click="beginStaking"
>
Begin staking
</button>
<button
v-if="farmerState === 'staked'"
class="nes-btn is-error mr-5"
#click="endStaking"
>
End staking
</button>
<button
v-if="farmerState === 'pendingCooldown'"
class="nes-btn is-error mr-5"
#click="endStaking"
>
End cooldown
</button>
<button class="nes-btn is-warning" #click="claim">
<!--Claim {{ availableA }} A / {{ availableB }} B-->
Claim $MEKA
</button>
</Vault>
</div>
<div v-else>
<div class="w-full text-center mb-5">
Farmer account not found :( Create a new one?
</div>
<div class="w-full text-center">
<button class="nes-btn is-primary" #click="initFarmer">
New Farmer
</button>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, nextTick, onMounted, ref, watch } from 'vue';
import useWallet from '#/composables/wallet';
import useCluster from '#/composables/cluster';
import { initGemFarm } from '#/common/gem-farm';
import { PublicKey } from '#solana/web3.js';
import ConfigPane from '#/components/ConfigPane.vue';
import FarmerDisplay from '#/components/gem-farm/FarmerDisplay.vue';
import Vault from '#/components/gem-bank/Vault.vue';
import { INFT } from '#/common/web3/NFTget';
import { stringifyPKsAndBNs } from '#gemworks/gem-farm-ts';
export default defineComponent({
components: { Vault, FarmerDisplay, ConfigPane },
setup() {
const { wallet, getWallet } = useWallet();
const { cluster, getConnection } = useCluster();
console.log('farm clust ' + cluster.value);
let gf: any;
watch([wallet, cluster], async () => {
await freshStart();
});
//needed in case we switch in from another window
onMounted(async () => {
await freshStart();
});
// --------------------------------------- farmer details
const farm = ref<string>(); // SO HERE AS WELL IT AS TO BE STATIC
const farmAcc = ref<any>();
const farmerIdentity = ref<string>();
const farmerAcc = ref<any>();
const farmerState = ref<string>();
const availableA = ref<string>();
const availableB = ref<string>();
//auto loading for when farm changes
watch(farm, async () => { // I ASSUME THAT THIS IS WATCHING THE FIELD TO GET THE VALUE
await freshStart();
});
const updateAvailableRewards = async () => {
availableA.value = farmerAcc.value.rewardA.accruedReward
.sub(farmerAcc.value.rewardA.paidOutReward)
.toString();
availableB.value = farmerAcc.value.rewardB.accruedReward
.sub(farmerAcc.value.rewardB.paidOutReward)
.toString();
};
const fetchFarn = async () => {
farmAcc.value = await gf.fetchFarmAcc(new PublicKey(farm.value!));
console.log(
`farm found at ${farm.value}:`,
stringifyPKsAndBNs(farmAcc.value)
);
};
const fetchFarmer = async () => {
const [farmerPDA] = await gf.findFarmerPDA(
new PublicKey(farm.value!),
getWallet()!.publicKey
);
farmerIdentity.value = getWallet()!.publicKey?.toBase58();
farmerAcc.value = await gf.fetchFarmerAcc(farmerPDA);
farmerState.value = gf.parseFarmerState(farmerAcc.value);
await updateAvailableRewards();
console.log(
`farmer found at ${farmerIdentity.value}:`,
stringifyPKsAndBNs(farmerAcc.value)
);
};
const freshStart = async () => {
if (getWallet() && getConnection()) {
gf = await initGemFarm(getConnection(), getWallet()!);
farmerIdentity.value = getWallet()!.publicKey?.toBase58();
//reset stuff
farmAcc.value = undefined;
farmerAcc.value = undefined;
farmerState.value = undefined;
availableA.value = undefined;
availableB.value = undefined;
try {
await fetchFarn();
await fetchFarmer();
} catch (e) {
console.log(`farm with PK ${farm.value} not found :(`);
}
}
};
const initFarmer = async () => {
await gf.initFarmerWallet(new PublicKey(farm.value!));
await fetchFarmer();
};
// --------------------------------------- staking
const beginStaking = async () => {
await gf.stakeWallet(new PublicKey(farm.value!));
await fetchFarmer();
selectedNFTs.value = [];
};
const endStaking = async () => {
await gf.unstakeWallet(new PublicKey(farm.value!));
await fetchFarmer();
selectedNFTs.value = [];
};
const claim = async () => {
await gf.claimWallet(
new PublicKey(farm.value!),
new PublicKey(farmAcc.value.rewardA.rewardMint!),
new PublicKey(farmAcc.value.rewardB.rewardMint!)
);
await fetchFarmer();
};
const handleRefreshFarmer = async () => {
await fetchFarmer();
};
// --------------------------------------- adding extra gem
const selectedNFTs = ref<INFT[]>([]);
const handleNewSelectedNFT = (newSelectedNFTs: INFT[]) => {
console.log(`selected ${newSelectedNFTs.length} NFTs`);
selectedNFTs.value = newSelectedNFTs;
};
const addSingleGem = async (
gemMint: PublicKey,
gemSource: PublicKey,
creator: PublicKey
) => {
await gf.flashDepositWallet(
new PublicKey(farm.value!),
'1',
gemMint,
gemSource,
creator
);
await fetchFarmer();
};
const addGems = async () => {
await Promise.all(
selectedNFTs.value.map((nft) => {
const creator = new PublicKey(
//todo currently simply taking the 1st creator
(nft.onchainMetadata as any).data.creators[0].address
);
console.log('creator is', creator.toBase58());
addSingleGem(nft.mint, nft.pubkey!, creator);
})
);
console.log(
`added another ${selectedNFTs.value.length} gems into staking vault`
);
};
return {
wallet,
farm,
farmAcc,
farmer: farmerIdentity,
farmerAcc,
farmerState,
availableA,
availableB,
initFarmer,
beginStaking,
endStaking,
claim,
handleRefreshFarmer,
selectedNFTs,
handleNewSelectedNFT,
addGems,
};
},
});
</script>
<style scoped></style>

Get next object in Array useState React

Iam building a quiz-website as practice for React and want to be able to change question with a button. When I change useState(0) manually to 1 the next object renders. But i cant get it to work with the button. When i click the button it jumps straight to the alert message.
function GetMovies() {
useEffect(() => {
fetchItems();
}, []);
const [items, setItems] = useState({ options: [] });
const fetchItems = async () => {
const data = await fetch("http://localhost:5000/api/movies");
const items = await data.json();
//test
//console.log(items[currentQuestion]);
setItems(items[currentQuestion]);
};
const [currentQuestion, setCurrentQuestion] = useState(0);
//change question solution that dont work
const HandleAnswerButtonClick = () => {
const nextQuestion = setCurrentQuestion + 1;
if (nextQuestion < items.length) {
setCurrentQuestion(nextQuestion);
} else {
alert("End of quiz");
}
setCurrentQuestion(nextQuestion);
};
return (
<div className="App">
<h1>Quiza</h1>
<div>
<span>Question 1</span>
</div>
<div>
<h3>Question: {items.description}</h3>
{items.options.map((c) => (
<button value={c.is_correct} key={c.text}>
{c.text}
</button>
))}
<div>
{//Next BUTTON}
<button onClick={() => HandleAnswerButtonClick()}>
Next question
</button>
</div>
{/* if-sats, om bild finns till frågan visas den, annars en class med display none */}
<div className="Q_pics">
{items.options.map((c) =>
!c.image ? (
<p className="Display_none">empty</p>
) : (
<img src={c.image} alt={c.text}></img>
)
)}
</div>
</div>
</div>
);
}
The mongoose schema from my API
const MovieSchema = mongoose.Schema(
{
category: { type: String, required: true },
description: { type: String, required: true },
image: {
type: String,
required: false,
},
options: [
{
text: {
type: String,
required: true,
},
is_correct: {
type: Boolean,
required: true,
default: false,
},
image: {
type: String,
required: false,
},
},
],
},
{ collection: "movies" }
);
That is because
const nextQuestion = setCurrentQuestion + 1;
should be
const nextQuestion = currentQuestion + 1;
Oh, and as #Nick Parsons mentioned in a comment, you also only store the first question in your state, since the fetchItems is only run once, and you do setItems(items[currentQuestion]);
You should do something like
function GetMovies() {
const [items, setItems] = useState([]);
const [currentQuestion, setCurrentQuestion] = useState(0);
const fetchItems = async () => {
const data = await fetch("http://localhost:5000/api/movies");
const items = await data.json();
//test
//console.log(items[currentQuestion]);
setItems(items);
};
//change question solution that dont work
const HandleAnswerButtonClick = () => {
const nextQuestion = currentQuestion + 1;
if (nextQuestion < items.length) {
setCurrentQuestion(nextQuestion);
} else {
alert("End of quiz");
}
setCurrentQuestion(nextQuestion);
};
useEffect(() => {
fetchItems();
}, []);
const activeQuestion = items[currentQuestion];
return (
<div className="App">
<h1>Quiza</h1>
<div>
<span>Question {currentQuestion + 1}</span>
</div>
<div>
<h3>Question: {activeQuestion && activeQuestion.description}</h3>
{activeQuestion && activeQuestion.options.map((c) => (
<button value={c.is_correct} key={c.text}>
{c.text}
</button>
))}
<div>
{//Next BUTTON}
<button onClick={() => HandleAnswerButtonClick()}>
Next question
</button>
</div>
{/* if-sats, om bild finns till frågan visas den, annars en class med display none */}
<div className="Q_pics">
{activeQuestion && activeQuestion.options.map((c) =>
!c.image ? (
<p className="Display_none">empty</p>
) : (
<img src={c.image} alt={c.text}></img>
)
)}
</div>
</div>
</div>
);
}
the code might be like this:
const [items, setItems] = useState({ options: [] });
// currentIdx might be a better name
const [currentIdx, setCurrentIdx] = useState(0);
const fetchItems = async () => {
const data = await fetch("http://localhost:5000/api/movies");
const items = await data.json();
// save all data as Nick said. and items should be type of Data[];
setItems(items);
};
const HandleAnswerButtonClick = () => {
const nextQuestion = currentIdx + 1;
if (nextQuestion < items.length) {
setCurrentIdx(nextQuestion);
} else {
alert("End of quiz");
}
};
// use displayItem for render
const displayItem = useMemo(() => items[currentIdx], [items, currentIdx]);
you'd better learn to use dev tools and watch values of your code I think 😂

React Question: Undefined Values passing data into props

I have been struggling to find a solution to this problem. I'm getting TypeError: Cannot read property 'Department_ID' of undefined errors from props being passed in from parent state. The main problem I'm seeing is that the Departments data is not being passed to the child element. Please look at the code and tell me what I'm missing! Thank you in advance for whatever help you can give me!
Parent Component: Positions
import {useEffect, useState} from 'react'
import API_access from '../../API_access'
import Position from './Position';
function Positions() {
const [positions, setPositions] = useState([]);
const [departments, setDepartments] = useState([]);
const [_position_ID, setPositionID] = useState('');
const [_title, setTitle] = useState('');
const [_department_ID, setDepartmentID] = useState();
const [_department_name, setDepartmentName] = useState();
useEffect(() => {
const get_data = async() => {
const jobs = await API_access('/API/Positions', 'GET');
const depts = await API_access('/API/Departments', 'GET');
setPositions(jobs.data);
setDepartments(depts.data);
}
get_data();
},[])
const Create_Position = async () => {
let new_position = {
Position_ID: _position_ID,
Position_Title: _title,
Department_ID: _department_ID
}
const success = await API_access('/API/Positions', 'POST', new_position);
if (success) {
let tmpArray = positions;
tmpArray.push(new_position);
setPositions(tmpArray);
}
}
const Update_Position = async (position) => {
const success = await API_access('/API/Positions', 'PATCH', position);
if (success) {
let tmpArray = positions.filter(pos => pos.Position_ID !== position.Position_ID);
tmpArray.push(position);
setPositions(tmpArray);
}
}
const Delete_Position = async (position) => {
const success = await API_access('/API/Positions', position);
if(success) {
let tmpArray = positions.filter(pos => pos.Position_ID !== position.Position_ID);
setPositions(tmpArray);
}
}
const Set_Department_Data = (event) => {
setDepartmentName(event.target.options[event.target.selectedIndex].text);
setDepartmentID(event.target.value);
}
return (
<div>
<form>
<input type="text" name="txtPositionID" value={_position_ID} onChange={event => setPositionID(event.target.value)}/>
<label htmlFor="txtPositionID">Position ID</label>
<input type="text" name="txtTitle" value={_title} onChange={event => setTitle(event.target.value)}/>
<label htmlFor="txtTitle">Job Title</label>
<select value={_department_ID} text={_department_name} onChange={event => Set_Department_Data(event)}>
<option value="" disabled>Select Department</option>
{departments.map((department, index) => (
<option key={index} value={department.Department_ID}>{department.Department_Name}</option>
))}
</select>
<button onClick={() => Create_Position}>Save</button>
</form>
<table>
<thead>
<tr>
<td>Edit</td>
<td>Position ID</td>
<td>Job Title</td>
<td>Department</td>
<td>Delete</td>
</tr>
</thead>
<tbody>
{positions.length > 0 && positions.map((position, index) => (
<Position key={index} Position={position} Departments={departments} Update_Position={Update_Position} Delete_Position={Delete_Position}/>
))}
</tbody>
</table>
</div>
)
}
export default Positions
Child Element: Position
import {useState, useEffect} from 'react'
function Position({ Position, Departments, Update_Position, Delete_Position }) {
const [_department_ID, setDepartmentID] = useState(Position.Department_ID);
const [_department_name, setDepartmentName] = useState('');
const [_job_title, setJobTitle] = useState(Position.Position_Title);
const [_edit, toggleEdit] = useState(false);
useEffect(() => {
const set_data = () => {
console.log(Departments)
console.log(_department_ID)
let dept = Departments.find(d => String(d.Department_ID) === String(_department_ID));
console.log(dept)
setDepartmentID(dept.Department_ID);
setDepartmentName(dept.Department_Name)
}
set_data();
}, [])
const Set_Department_Data = (event) => {
setDepartmentName(event.target.options[event.target.selectedIndex].text);
setDepartmentID(event.target.value);
}
const Update_This_Position = () => {
if (_edit) {
let pos = {
Position_ID: Position.Position_ID,
Position_Title: _job_title,
Department_ID: _department_ID
}
Update_Position(pos);
}
toggleEdit(!_edit);
}
if (_edit) {
return (
<tr>
<td><button onClick={() => Update_This_Position()}>Save</button></td>
<td>{Position.Position_ID}</td>
<td><input type="text" value={_job_title} onChange={event => setJobTitle(event.target.value)}/> </td>
<td><select value={_department_ID} text={_department_name} onChange={event => Set_Department_Data(event)}>
{Departments.map((department, index) => (
<option key={index} value={department.Department_ID}>{department.Department_Name}</option>
))}
</select></td>
<td><button onClick={() => Delete_Position(Position)}>X</button></td>
</tr>
)
} else {
return (
<tr>
<td><button onClick={() => Update_This_Position()}>></button></td>
<td>{Position.Position_ID}</td>
<td>{Position.Position_Title}</td>
<td>{_department_name}</td>
<td><button onClick={() => Delete_Position(Position)}>X</button></td>
</tr>
)
}
}
export default Position
Error:
TypeError: Cannot read property 'Department_ID' of undefined
set_data
src/components/Personnel/Position.js:15
12 | console.log(_department_ID)
13 | let dept = Departments.find(d => String(d.Department_ID) === String(_department_ID));
14 | console.log(dept)
> 15 | setDepartmentID(dept.Department_ID);
| ^ 16 | setDepartmentName(dept.Department_Name)
17 | }
18 | set_data();
View compiled
(anonymous function)
src/components/Personnel/Position.js:18
15 | setDepartmentID(dept.Department_ID);
16 | setDepartmentName(dept.Department_Name)
17 | }
> 18 | set_data();
| ^ 19 | }, [])
20 |
21 | const Set_Department_Data = (event) => {
View compiled
▶ 16 stack frames were collapsed.
get_data
src/components/Personnel/Positions.js:18
15 | const jobs = await API_access('/API/Positions', 'GET');
16 | const depts = await API_access('/API/Departments', 'GET');
17 | setPositions(jobs.data);
> 18 | setDepartments(depts.data);
| ^ 19 | }
20 | get_data();
21 | },[])
View compiled
This screen is visible only in development. It will not appear if the app crashes in production.
Open your browser’s developer console to further inspect this error. Click the 'X' or hit ESC to dismiss this message.
Issue
Array.prototype.find can return undefined if no element passes the predicate condition.
The find() method returns the value of the first element in the
provided array that satisfies the provided testing function. If no
values satisfy the testing function, undefined is returned.
During the initial render cycle departments is an empty array, so dept is all but guaranteed to be undefined on the initial render.
const set_data = () => {
console.log(Departments)
console.log(_department_ID)
let dept = Departments.find(d => String(d.Department_ID) === String(_department_ID));
console.log(dept)
setDepartmentID(dept.Department_ID);
setDepartmentName(dept.Department_Name)
}
Solution
You should check for valid results from the find before accessing the result.
const set_data = () => {
const deptId = String(_department_ID);
const dept = Departments.find(d => String(d.Department_ID) === deptId);
if (dept) {
setDepartmentID(dept.Department_ID);
setDepartmentName(dept.Department_Name);
}
}
BUT, this code is running in an useEffect hook with an empty dependency, so the hook runs only once when the component mounts. You've a couple options:
Add a dependency to the useEffect hook to set the component state when the Departments prop updates.
useEffect(() => {
const set_data = () => {
const deptId = String(_department_ID);
const dept = Departments.find(d => String(d.Department_ID) === deptId);
if (dept) {
setDepartmentID(dept.Department_ID);
setDepartmentName(dept.Department_Name);
}
}
set_data();
}, [Departments]); // <-- dependency
Conditionally render Position only when all of the props are populated. Ensure both positions and departments arrays have a truthy (non-zero) length.
<tbody>
{positions.length && departments.length && positions.map((position, index) => (
<Position
key={index}
Position={position}
Departments={departments}
Update_Position={Update_Position}
Delete_Position={Delete_Position}
/>
))}
</tbody>

Error: Too many re-renders when trying to setState

This is my code:
import React, {useState, useEffect} from 'react';
import './App.css';
import {Table, Button, InputGroup, FormControl} from 'react-bootstrap';
import {PonCard} from "./components/PonCard";
function App() {
const [pons, setPons] = useState(null);
const [translations, setTranslations] = useState(null);
const [isInEditMode, setIsInEditMode] = useState(false);
const [inputValue, setInputValue] = useState('');
const [errors, setErrors] = useState([]);
const [translationsToSave, setTranslationsToSave] = useState([]);
const changeIsInEditMode = () => setIsInEditMode(!isInEditMode);
const handleEditButtonClick = (id) => console.log('Edit', id);
const handleDeleteButtonClick = (id) => console.log('Delete', id);
const handleInputChange = (e) => setInputValue(e.target.value);
const handleFetchOnButtonClick = async () => {
const resp = await fetch(`http://localhost:8080/pons/findTranslation/${inputValue}`).then(r => r.json()).catch(e => console.log(e));
if (resp.ok === true) {
setTranslations(resp.resp[0].hits);
setErrors([]);
} else {
setErrors(resp.errors ? resp.errors : ['Something went wrong. check the input']);
}
};
const handleSaveTranslations = async () => {
const resp = await fetch('http://localhost:8080/pons/', {
method: 'POST',
body: JSON.stringify({original: inputValue, translations: translationsToSave}),
mode: 'cors',
headers: {
'Content-Type': 'application/json',
}
}).then(r => r.json())
.catch(e => {
console.log(e);
return {ok: false};
});
setInputValue('');
setTranslations(null);
if (resp.errors) {
setErrors(resp.errors);
}
};
useEffect(() => {
fetch('http://localhost:8080/pons/')
.then(r => r.json())
.then(resp => {
if (resp.ok === true) {
setPons(resp.pons);
} else {
setErrors(resp.errors);
}
})
.catch(e => console.log(e));
}, []);
return (
<div className="App">
<InputGroup className="mb-3">
<FormControl
value={inputValue}
onChange={handleInputChange}
placeholder={inputValue}
/>
</InputGroup>
<div className="mb-3">
<Button onClick={handleFetchOnButtonClick} disabled={inputValue === '' || errors.length > 0}>Translate</Button>
<Button onClick={changeIsInEditMode}>
{isInEditMode ? 'Exit edit mode' : 'Enter edit mode'}
</Button>
<Button disabled={translationsToSave.length === 0} onClick={handleSaveTranslations}>Save translations</Button>
</div>
{errors.length > 0 ? errors.map(e => <div key={e}>{e}</div>) : null}
{
pons && !translations && inputValue === '' ? pons.map(pon => <PonCard key={Math.random()} {...{pon}}/>) : null
}
{
translations ?
<Table striped bordered hover>
<thead>
<tr>
<th>Original</th>
<th>Translation</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{
translations.map(pon => pon.roms.map(rom => rom.arabs.map(arab => arab.translations.map(translation => {
const {source, target} = translation;
return (
<tr key={Math.random()}>
<td><span dangerouslySetInnerHTML={{__html: source}}/></td>
<td><span dangerouslySetInnerHTML={{__html: target}}/></td>
<td>
{
!translationsToSave.includes(target) ?
<Button onClick={() => {
setTranslationsToSave(prev => [...prev, target]);
}}>
Add translation
</Button>
:
<Button
onClick={() => {
setTranslationsToSave((prev) => {
const index = prev.findIndex(elem => elem === target)
return [...prev.slice(0, index), ...prev.slice(index + 1)]
});
}}>
Remove translation
</Button>
}
</td>
</tr>
)
}))))
}
</tbody>
</Table>
: (
<span>No translations</span>
)
}
</div>
);
}
export default App;
PonCard component:
import {Button, Card} from "react-bootstrap";
import React, {useState} from "react";
export const PonCard = ({pon}) => {
const [isFlipped, setIsFlipped] = useState(false);
const handleClick = setIsFlipped(!isFlipped);
return (
<Card style={{width: '18rem'}}>
<Card.Body>
<Card.Title>{pon.original}</Card.Title>
<Card.Text>
{pon.translations.map(translation => (
<div key={Math.random()} dangerouslySetInnerHTML={translation}/>
))}
</Card.Text>
<Button variant="primary" onClick={handleClick}>Show translations</Button>
</Card.Body>
</Card>
)
};
What I'm trying to do is to fetch data on mount. I found that this is the correct way to mimic componentDidMount
useEffect(() => {
fetch('http://localhost:8080/pons/')
.then(r => r.json())
.then(resp => {
if (resp.ok === true) {
setPons(resp.pons);
} else {
setErrors(resp.errors);
}
})
.catch(e => console.log(e));
}, []);
But I get
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
All the time.
106 | .then(r => r.json())
107 | .then(resp => {
108 | if (resp.ok === true) {
> 109 | setPons(resp.pons);
| ^ 110 | } else {
111 | setErrors(resp.errors);
112 | }
It points to the setPons method, which makes no sense, since it's only updated once on mount. What am I missing?
The issue is this line in PonCard:
const handleClick = setIsFlipped(!isFlipped);
Every time PonCard renders, this line will immediately toggle its flipped state, which renders it again and flips it again, and so on. You probably intended to do this instead:
const handleClick = () => setIsFlipped(!isFlipped);
The reason the error message points to setPons is just that that's the first set state that kicked it off. Prior to that, no PonCard was being rendered, and so there was no infinite loop of PonCard renders.

Categories

Resources