I am making Quiz app in React and I got stuck in a problem where Option component gets re-render itself after clicking each option.
Here is the code
App.js
Main app
export default function App() {
const [questions, setQuestions] = useState([])
const [score, setScore] = useState(0)
// Fetching questions
useEffect(() => {
async function fetchQuestions(){
const res = await fetch("https://opentdb.com/api.php?amount=10&category=18&difficulty=medium")
const data = await res.json()
setQuestions(data.results)
}
fetchQuestions()
}, [])
// Checking answer on clicking any option
const checkAnswer = (option, questionIndex) => {
if(option === questions[questionIndex].correct_answer){
setScore(prevScore => prevScore+=5)
console.log("correct")
}
else{
setScore(prevScore => prevScore-=1)
console.log("incorrect")
}
}
// Main Screen
return (
<QuizScreen questions={questions} score={score} checkAnswer={checkAnswer} />
)
}
QuizScreen.js
Component for rendering quiz screen
export default function QuizScreen(props) {
// Setting questions
const question = props.questions.map((ques, index) => {
// storing options
const opt = []
opt.push(ques.correct_answer)
opt.push(ques.incorrect_answers[0])
ques.incorrect_answers[1] && opt.push(ques.incorrect_answers[1]) // if option 3 available
ques.incorrect_answers[2] && opt.push(ques.incorrect_answers[2]) // if option 4 available
// Arranging options in random order
for(let i=0; i<opt.length; i++){
let j = Math.floor(Math.random() * (i+1))
let temp = opt[i]
opt[i] = opt[j]
opt[j] = temp
}
// Setting options
const option = opt.map(opt => <Option key={nanoid()} option={opt} questionIndex={index} checkAnswer={props.checkAnswer} />)
// Rendering Questions
return (
<div className="ques-container" key={nanoid()}>
<p className="ques-title">{ques.question}</p>
{option}
</div>
)
})
// Main Screen
return (
<div>
<p>{props.score}</p>
{question}
</div>
)
}
Option.js
Component for rendering option buttons
export default function Option(props) {
const [selected, setSelected] = useState(false)
const btnStyle = {
backgroundColor: selected ? "#D6DBF5" : "#FFFFFF"
}
return (
<button
className="ques-option"
onClick={() => {
props.checkAnswer(props.option, props.questionIndex)
setSelected(prevState => !prevState)
}}
style={btnStyle}
>
{props.option}
</button>
)
}
I tried to make Option component separately, but it did not work out
Wrap this around a useMemo
const question = useMemo(() => {
return props.questions.map((ques, index) => {
// storing options
const opt = []
opt.push(ques.correct_answer)
opt.push(ques.incorrect_answers[0])
ques.incorrect_answers[1] && opt.push(ques.incorrect_answers[1]) // if option 3 available
ques.incorrect_answers[2] && opt.push(ques.incorrect_answers[2]) // if option 4 available
// Arranging options in random order
for(let i=0; i<opt.length; i++){
let j = Math.floor(Math.random() * (i+1))
let temp = opt[i]
opt[i] = opt[j]
opt[j] = temp
}
// Setting options
const option = opt.map(opt => <Option key={nanoid()} option={opt} questionIndex={index} checkAnswer={props.checkAnswer} />)
// Rendering Questions
return (
<div className="ques-container" key={nanoid()}>
<p className="ques-title">{ques.question}</p>
{option}
</div>
)
})
}, [props.questions])
Related
I'm testing a React app for the first time and I'm struggling to write tests that check if an array in a component has 2 elements on first render and on clicking a button.
The error I'm getting is TypeError: Expected container to be an Element, a Document or a DocumentFragment but got string.
Here's the component where I need to test usersCards - it needs to have two elements on first render and every time the user clicks 'deal'.
I'm not sure how to deal with variables in components - do I mock it up in the test file? Ant help appreciated!
\\imports
export default function Home(){
const startHandSize = 2
const [starterDeck, setStarterDeck] = useState(shuffle(deckArray))
const [howManyDealt, setHowManyDealt] = useState(startHandSize)
const [total, setTotal] = useState(0)
const [ace, setAce] = useState(0)
const deal = () => {
setHowManyDealt(startHandSize)
setStarterDeck(shuffle(deckArray))
setAce(0)
}
const hit = () => !bust && setHowManyDealt(prev => prev + 1)
const usersCards = starterDeck.slice(-howManyDealt)
const bust = total > 21;
useEffect(() => {
setTotal(usersCards.reduce((a, e) => a + e.value, 0) + ace)
}, [ace, usersCards])
return(
<div>
{
<>
<button data-testid="deal" onClick={deal}>DEAL</button>
<button data-testid="hit" disabled={bust} onClick={hit}>HIT</button>
<button disabled={bust}>STAND</button>
<Total total={total}/>
{usersCards.map(card => (
<Card data-testid="test-card" key={card.index}
card={card} setTotal={setTotal} total={total}
ace={ace} setAce={setAce}
/>
))}
</>}
</div>
)
}
Here's the test:
//Deal button test
test("on initial render, two cards are displayed", () => {
render(<Home />)
const cards = getAllByTestId('test-card')
expect(cards.length).toEqual(2)
})
I guess something like that would work:
test("on initial render, two cards are displayed", () => {
const { getAllByTestId } = render(<Home />);
const cards = getAllByTestId('test-card');
expect(cards.length).toEqual(2);
});
test("two new cards should be displayed after clicking the button", () => {
const { getAllByTestId, getByTestId } = render(<Home />);
const dealButton = getByTestId('deal');
fireEvent.click(dealButton);
const cards = getAllByTestId('test-card');
expect(cards.length).toEqual(2);
});
I am trying to render a huge list of data in React. I know I can use react-window for this usecase but wanted to try if we can implement a similar window based rendering using Intersection Observer API.
I have written this component to try the same. But Here my component is rendering the whole 10,000 divs even if it is not in view port as i am iterating over the data. Is there any way to prevent rendering if the element is not there in viewport similar to react-window. Thank you in advance.
import React from 'react';
import './CustomVirtualizedList.css';
import faker from 'faker';
const generateFakeData = (() => {
const data = [];
for (let i = 0; i < 10000; i++) {
data.push({ id: i, selected: false, label: faker.address.state() })
}
return () => data;
})();
function getRandomColor() {
var letters = '0123456789ABCDEF';
var color = '#';
for (var i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
const ListElement = (props) => {
const [visible, setVisible] = React.useState(false);
const {containerRef} = props;
const elementRef = React.useRef();
let intersectionObserver;
const onVisibilityChange = ([entry]) => {
setVisible(entry.isIntersecting)
}
React.useEffect(() => {
console.log(props);
intersectionObserver = new IntersectionObserver(onVisibilityChange, containerRef.current);
intersectionObserver.observe(elementRef.current);
return () => {
intersectionObserver.disconnect()
}
}, [])
return <div
ref={elementRef}
style={{ backgroundColor: getRandomColor() }}
className="listElement">
{visible ? 'I am visible' : 'I am not visible'}
</div>
}
export const ListContainer = () => {
const containerRef = React.useRef();
const [data, setData] = React.useState(generateFakeData())
return (
<div className="listContainer">
{data.map(val => {
return <ListElement containerRef={containerRef} {...val} />
})}
</div>
);
};
hello iam creating an ecomerce shop in my chec dasboard i set variants of sizes large small but when i try to implement options drop down on options array i get the error Cannot read property '0' of undefined in line12 which is let finalSizeArray = props.product.variants[0].options.map(option =
productcard.js
import React, { useState, useEffect } from 'react';
import { Card, Image, Button, Icon, Dropdown } from 'semantic-ui-react';
const ProductCard = (props) => {
console.log(props.product, 'props from Container')
const [sizes, setSizes] = useState([])
const [variantInfo, setVariantInfo] = useState()
useEffect(() => {
let finalSizeArray = props.product.variants[0].options.map(option => {
let sizeInfo = {}
sizeInfo.key = option.name
sizeInfo.text = option.name
sizeInfo.value = option.id
return sizeInfo
})
setSizes(finalSizeArray)
}, [])
const handleSize = (e, {value}) => {
setVariantInfo({[props.product.variants[0].id]: value})
}
const handleButtonAddCart = e => {
e.preventDefault()
props.addToCart(props.product.id, variantInfo)
// Funtion to Clear Select Input for Dropdown - Needs work.
// let selectInput = document.querySelectorAll('.sizes-drop')
// selectInput.forEach((input,i) => {
// input.children[0].innerHTML = 'Select Size'
// // input.children[0].classList.add('default')
// })
}
return (
<Card>
<Image src={props.product.media.source} />
<Card.Content>
<Card.Header>{props.product.name}</Card.Header>
<Card.Meta>{props.product.price.formatted_with_symbol}</Card.Meta>
<Card.Description>{props.product.description.replace(/(<([^>]+)>)/ig,"")}</Card.Description>
<Dropdown
className="sizes-drop"
fluid
placeholder='Select Size'
selection
options={sizes}
/>
<Button fluid className='add-button' onClick={handleButtonAddCart}>
Add to Cart
<Icon name='arrow right' />
</Button>
</Card.Content>
</Card>
);
};
export default ProductCard;
I thinks its because your props not yet loaded...
You can give handle to your code like this
useEffect(() => {
let finalSizeArray = props?.product?.variants[0]?.options?.map(option => {
let sizeInfo = {}
sizeInfo.key = option.name
sizeInfo.text = option.name
sizeInfo.value = option.id
return sizeInfo
})
setSizes(finalSizeArray)
}, [])
I'm trying to use react forwardRef to call a function inside bunch of child components. Here is the code.
const WorkoutFeedbackForm = ({
latestGameplaySession,
activityFeedbacks,
selectedActivityIndex,
setIsReady,
}) => {
const [isLoading, setIsLoading] = useState(false);
const workoutRef = createRef();
const refMap = new Map();
const onSubmitFeedbackClick = useCallback(async () => {
setIsLoading(true);
await workoutRef.current.onSubmitFeedback();
for (let i = 0; i < activityFeedbacks.length; i++) {
const activityRef = refMap.get(activityFeedbacks[i].sessionID);
console.log(activityRef);
if (activityRef && activityRef.current) {
activityRef.current.onSubmitFeedback();
}
}
setIsLoading(false);
}, [
activityFeedbacks,
refMap,
]);
return (
<>
<FeedbackFormContainer
key={`${latestGameplaySession.id}-form`}
name="Workout Feedback"
feedback={latestGameplaySession.coachFeedback}
isSelected
gameplaySessionDoc={latestGameplaySession}
pathArr={[]}
ref={workoutRef}
/>
{activityFeedbacks.map((feedback, index) => {
const activityRef = createRef();
refMap.set(feedback.sessionID, activityRef);
return (
<FeedbackFormContainer
key={feedback.sessionID}
name={feedback.name}
feedback={feedback.coachFeedback}
isSelected={index === selectedActivityIndex}
gameplaySessionDoc={latestGameplaySession}
pathArr={feedback.pathArr}
setIsReady={setIsReady}
ref={activityRef}
/>
);
})}
<FeedbackSubmit
onClick={onSubmitFeedbackClick}
isLoading={isLoading}
>
Save Feedbacks
</FeedbackSubmit>
</>
);
};
The problem is it seems createRef only works for the component outside the loop. Do you have any idea what's wrong here. Or is it not possible to do that?
I'm doing this fullstack course to learn about web dev: https://fullstackopen.com/en/part2/getting_data_from_server
And I have a problem with section 2.13*.
I am able to display a list of the countries after filtering with the button. Pressing the button returns the correct values from the countries arrays as seen with the console.log(country), but it doesn't to the screen.
My guess is that I can't return a div item within another item, but I am pretty sure that works in normal cases, so the fact that I'm returning the item to a different return statement might be the issue?
How can I fix this? I know my code is messy and a refactor might make things simpler, but it is currently beyond me right now since I find it easier to refactor working code.
In the DisplayCountries component, I've tried apply a map to countries that fit the filter input and prints it into a div item. Now when I add a button beside it, it displays correctly, but pressing it does not yield what I expect.
Is the correct approach here to use a useState with the button, so that each button click will rerender the screen? How would I go about doing this if so?
After pressing the button, the detailed information of the country should display such as in 2.12* from the linked website.
import { useState, useEffect } from 'react'
import axios from 'axios'
//feed array of countries
const printLanguages = (languages) => {
// console.log('map', languages.map(language => language.name))
return languages.map(language => <li key={language.name}>{language.name}</li>)
}
const displayCountryView = (country) => {
console.log(country)
return (
<div>
<h1>{country.name}</h1>
<p>capital {country.capital}</p>
<p>population {country.population}</p>
<h2>languages</h2>
<ul>
{printLanguages(country.languages)}
</ul>
<img src={country.flag} height="100" width="100"></img>
</div>
)
}
const DisplayCountries = ({ countries, searchValue }) => {
const displayFilter = filteredCountries(countries, searchValue)
// console.log('current search', searchValue)
if (displayFilter.length >= 10) {
return <p>Too many matches, specify another filter</p>
} else if (isFiltered(searchValue)) {
if (displayFilter.length > 1 && displayFilter.length < 10) {
console.log('new level')
return displayFilter.map(country => <div key={country.name}>{country.name}{showButton(country)}</div>)
} else if (displayFilter.length === 1) {
// console.log('suh')
// return displayFilter.map(country => <p key={country.name}>{country.name}</p>)
const country = displayFilter
return displayCountryView(country[0])
// console.log(country)
// console.log('country.name', country[0])
// console.log(country[0].languages)
// console.log(printLanguages(country[0].languages))
// return (
// <div>
// <h1>{country[0].name}</h1>
// <p>capital {country[0].capital}</p>
// <p>population {country[0].population}</p>
// <h2>languages</h2>
// <ul>
// {printLanguages(country[0].languages)}
// </ul>
// <img src={country[0].flag} height="100" width="100"></img>
// </div>
// )
}
} else {
return <p>empty</p>
}
}
const showButton = (country) => {
return <button type="button" onClick={() => displayCountryView(country)}>show</button>
}
const filteredCountries = (countries, searchValue) => {
const showCountries = (!isFiltered(searchValue))
? [{ name: "hi" }]
: countries.filter(country => country.name.toLowerCase().includes(searchValue.toLowerCase()))
// const countryMap = countries.map(country => country.name.toLowerCase())
// console.log(countryMap)
// return countryMap
return showCountries
}
function isFiltered(value) {
if (value === '') {
return false
} else {
return true
}
}
const Filter = ({ search, onChange }) => {
return (
<form >
<div>
find countries <input value={search} onChange={onChange} />
</div>
</form>
)
}
const App = () => {
const [countries, setCountries] = useState([])
const [search, setNewSearch] = useState('')
const [showCountry, setShowCountry] = useState('false')
useEffect(() => {
// console.log('effect')
axios
.get('https://restcountries.eu/rest/v2/all')
.then(response => {
// console.log('promise fulfilled')
setCountries(response.data)
})
}, [])
// const countryNames = countries.map(country => country.name)
// console.log('name', countryNames)
const handleSearchChange = (event) => {
setNewSearch(event.target.value)
}
// const fil = countries.filter(country => country.name==='Afg')
// console.log(countries[0])
// console.log('filtered:',fil)
// console.log(countries[0])
// console.log('render', countries.length, 'persons')
return (
<div>
<Filter search={search} onChange={handleSearchChange} />
<form>
<div>
<DisplayCountries countries={countries} searchValue={search} />
</div>
</form>
</div>
)
}
export default App;