react component rendering after state change working in delay - reactjs

I have those 2 components
FormComponent
const FormComponent = (props) => {
//keep the current step in the component state
const [currentStep, setCurrentStep] = useState(props.step);
const [componentData, setComponentData] = useState(null);
const [componentQuestions, setQuestions] = useState(null);
const [errors, setErrors] = useState();
const updateUserResponse = props.updateUserResponse;
const renderStepsTracker = props.renderStepsTracker;
/**
* Get the component content after mount
*/
useEffect(() => {
if (componentData) {
setQustionsData(componentData);
}
//fire if componentData has changed or error object is changed
}, [errors,componentData]);
/**
* Get the component content after mount
*/
useEffect(() => {
if (currentStep && !componentData) {
let stepData = currentStep.stepdata;
stepData.update = getUpdateTime();
setComponentData(stepData);
}
});
/**
* Get the current time in milliseconds
*/
const getUpdateTime = () => {
var d = new Date();
var n = d.getTime();
return n
}
/**
* Extract the questions from the step object and
* set the state accordingly
*/
const setQustionsData = (stepData) => {
let questions = stepData.questions;
//assign a react component to each question
questions.map((question, key) => {
const component = getComponent(question);
const question_id = question.question_id;
questions[key].component = component;
questions[key].error = (null != errors && 'undefined' !== typeof errors[question_id]) ? errors[question_id] : '';
})
setQuestions(questions);
}
/**
* a callback function to validate the current form
* #param {object} step
*/
const validateForm = (step) => {
const formErrors = props.validateForm(step);
setErrors(formErrors)
}
/**
* Render the form
*/
const renderForm = () => {
return null !== componentQuestions ? (
<>
<div className="row full-height">
<div className="column">
<div className="form-wrapper">
<div className="questions">
{componentQuestions.map(question => {
return (
<>
{question.component}
</>
)
})}
</div>
<NextStepButton
step={currentStep}
nextStepCallback={props.jumpToStep}
validateForm={validateForm}
/>
</div>
</div>
</div>
</>
) : ''
}
/**
* Render the image in case it exists
*/
const renderImage = () => {
return (
<div className="full-height">
<img className="full-height-image" src={componentData.image} alt="" />
</div>
)
}
/**
* Get the question component
*/
const getComponent = (question) => {
switch (question.question_type) {
case "text":
return (
<div className="field-wrap">
<label>
<span className="question-label">{question.question_label}</span>
<span className="question-title">{question.question_title}</span>
<input type="text"
name={question.question_id}
onChange={updateUserResponse}
required={question.required}
/>
{question.error ?
<span className={"error-message " + question.error.reason}>{question.error_message}</span>
: ''}
</label>
</div>
)
}
}
return (
componentData ?
<>
<div className="row full-height expanded">
{componentData.image ?
<div className="column full-height col-collapse image-wrap">
{renderImage()}
</div>
: ''}
<div className="column full-height questions-container">
{renderStepsTracker()}
{renderForm()}
</div>
</div>
</>
: ''
)} enter code hereexport default FormComponent
**
NextStepButton
**
const NextStepButton = (props) => {
const step = props.step
const validateForm = props.validateForm
return props.step.nextButtonText ? (
<span className="next-step-button" onClick={() => {
validateForm( step )
}}>
{step.nextButtonText}
</span>
) : ''
}
export default NextStepButton
When the user clicks on the next step the form is validated and returns an array of errors,
The array of errors is set to the component state and then supposed to display the error
For some reason the error appears only after the user clicks 3 times on the next step button
Why is that ?
EDIT
this is the main component that contains the global form validation
const MainContainer = () => {
//set state variables
const [nextButtonText, setNextButtonText] = useState()
const [previousButtonText, setPreviousButtonText] = useState()
const [backButtonCls, setBackButtonCls] = useState()
const [currentStep, setCurrentStep] = useState()
const [stepsData, setStepsData] = useState()
const [currentStepNum, setCurrentStepNum] = useState(null)
const [classNames, setclassNames] = useState(null)
const [userData, setUserData] = useState({})
//set refrence for callbacks that use this component state
const ref = useRef({
stepsData: stepsData,
currentStep: currentStep
})
/**
* On Initial state
*/
useEffect(() => {
if (!stepsData) {
getStepsData().then((response) => {
initProcess(response)
})
}
})
/**
* Init the process after getting data from the server
*/
const initProcess = (response) => {
const steps = setStepsComponents(response.data.steps)
const initialStep = steps[0];
ref.current.stepsData = steps
setStepsData(steps)
setStep(initialStep, 1)
}
/**
* Form submission callback
* Loop over step questions and check for the appropriate userdata
*/
const validateForm = (submittedStep) => {
let errors = {};
if ('null' !== typeof submittedStep.stepdata.questions) {
const questions = submittedStep.stepdata.questions
questions.map((question, key) => {
const question_name = question.question_id
//if there is an error add it to the questions collection
if (question.required && !userData[question_name]) {
errors[question.question_id] = {
reason : 'required'
}
}
})
}
if (errors) {
return errors
}
return true;
}
/**
* Capture user settings on change
*/
const updateUserResponse = (event) => {
userData[event.target.name] = event.target.value;
setUserData(userData)
}
/**
* Set the appropriate step component
* #param {OBJECT} steps
*/
const setStepsComponents = (steps) => {
steps.map((step, key) => {
switch (steps[key]['component']) {
case "onboarding":
steps[key]['component'] = <Onboarding step={step} setUserData />
break;
case "questions":
steps[key]['component'] = <FormComponent
step={step}
renderStepsTracker={renderStepsTracker}
updateUserResponse={updateUserResponse}
validateForm={validateForm}
/>
break;
}
})
return steps
}
/**
* Callback function that creates the steps tracker on each component
*/
const renderStepsTracker = () => {
return ref.current.stepsData ? (
<>
<div className="stepsTracker">
<div className="row">
<div className="column">
<div className="stepsTrackerWrap">
{ref.current.stepsData.map((step, key) => {
const activeClass = ref.current.currentStep === key ? "active" : ""
return key > 0 ? (
<>
<div className={"stepTracker " + activeClass}>
<div className="stepTrackerImage">
<img src={step.icon} alt="" />
</div>
<label>{step.stepdata.title}</label>
</div>
<div className="stepSpacer"></div>
</>
) : ''
})}
</div>
</div>
</div>
</div>
</>
) : ''
}
/**
* Render the steps component if data is available
*/
const renderSteps = () => {
return stepsData ? (
<StepZilla
steps={stepsData}
onStepChange={onstepChange}
nextButtonText={nextButtonText}
showSteps={false}
backButtonText={previousButtonText}
backButtonCls={backButtonCls}
/>
) : ''
}
/**
* Set the data for the current step
* #param {object} step
*/
const setStep = (step, stepNum) => {
setNextButtonText(step.showNextButton ? step.nextButtonText : true)
setPreviousButtonText(step.previousButtonText)
setBackButtonCls(step.backButtonCls)
setCurrentStep(step)
setCurrentStepNum(stepNum)
ref.current.currentStep = stepNum
//add bottom padding in case the next button appears
if (step.showNextButton) {
setclassNames('padding')
} else {
setclassNames(' ')
}
}
/**
* Perform actions when step is changed
* #param {step} step
*/
const onstepChange = (stepNum) => {
if ('undefined' === typeof stepNum) {
stepNum = '0';
}
setCurrentStepNum(stepNum)
const nextStep = stepsData[stepNum];
setStep(nextStep, stepNum)
}
return (
<>
<div className={'step-progress ' + classNames}>
{renderSteps()}
</div>
</>
)
}
export default MainContainer

Related

How to check if the value of one obj exists in the another obj?

I wanna make follow/unfollow toggle button, and following / follower list(object in array) will be called seperately from server.
Follower list needs to have both unfollow/follow button status.
When I call follower list, how can I check the IDs of the people who follow me matches the IDs of my following list & reflect in on the button?
example following, follower object in array
[{id: 1, profileImg: xxx},{id: 2, profileImg: xxx},{id: 3, profileImg: xxx}... ]
my code in js below
const { select } = props;
const [choice, setChoice] = useState(select);
const [followingList, setFollowingList] = useState([]);
const [followerList, setFollowerList] = useState([]);
const handleChoice = (e) => {
setChoice(e.target.value);
};
useEffect(() => {
getFollowing()
.then((res) => {
setFollowingList(res);
})
.then(
getFollower().then((res) => {
setFollowerList(res);
}),
);
}, []);
my code in html
<Container onClick={(e) => e.stopPropagation()}>
<TogglebtnContainer>
<ToggleBtn onClick={handleChoice} value="following" choice{choice}>Following</ToggleBtn>
<ToggleBtn onClick={handleChoice} value="follower" choice={choice}>Follower</ToggleBtn>
</TogglebtnContainer>
<FollowContainer>
<Follow>
{choice === 'following'? (followingList.map((follow, idx) => {
return (
<div className="follow-item" key={idx}>
<div className="follow-img"><img src={follow.profileImg} alt="UserPic" /> </div>
<div className="follow-name">{follow.nickname}</div>
<FollowBtn key={follow.id}>Unfollow</FollowBtn></div>
);})
: (followerList.map((follow, idx) => {
return (
<div className="follow-item" key={idx}>
<div className="follow-img">
<img src={follow.profileImg} alt="UserPic" />
</div>
<div className="follow-name">{follow.nickname}</div>
<FollowBtn key={follow.id}>follow</FollowBtn>
</div>
})}
</Follow>
</FollowContainer>
</Container>
I thought I could check if this IDs matches IDs of my following list and create a new boolean state.
(ex [isFollowing, setIsFollowing = useState(false)) but couldn't find a way.
getFollower().then((res) => {
setFollowerList(res);
To know which followers the user is already following and follow/unfollow followers
short answer, set a flag when loading the data
useEffect(() => {
let isValidScope = true;
const fetchData = async () => {
const followingList = await getFollowing();
if (!isValidScope) { return; }
setFollowingList(followingList);
let followersList = await getFollower();
if (!isValidScope) { return; }
const followingUserIds = followingList?.map(f => f.id)
followersList = followersList?.map(follower => {
return followingUserIds?.includes(follower.id) ?
{ ...follower, isFollowing: true } : follower
}
setFollowerList(followersList)
}
fetchData()
return () => { isValidScope = false }
}, []);
const onFollowFollower = (followerId) => {
const followersList = followerList?.map(follower => {
return follower.id === followerId ?
{ ...follower, isFollowing: true } : follower
}
setFollowerList(followersList)
}
const onUnfollowFollower = (followerId) => {
const followersList = followerList?.map(follower => {
return follower.id === followerId ?
{ ...follower, isFollowing: false } : follower
}
setFollowerList(followersList)
}
Render code
<Follow>
{choice === 'following'? (followingList.map((follow, idx) => {
return (
<div className="follow-item" key={idx}>
<div className="follow-img"><img src={follow.profileImg} alt="UserPic" /> </div>
<div className="follow-name">{follow.nickname}</div>
<FollowBtn key={follow.id}>Unfollow</FollowBtn>
</div>
);})
: (followerList.map((follow, idx) => {
return (
<div className="follow-item" key={idx}>
<div className="follow-img">
<img src={follow.profileImg} alt="UserPic" />
</div>
<div className="follow-name">{follow.nickname}</div>
{ follow?.isFollowing ? <FollowBtn () => onUnfollowFollower(follow.id)>Unfollow</FollowBtn> : null }
{ !follow?.isFollowing ? <FollowBtn onClick={() => onFollowFollower(follow.id)>Follow</FollowBtn> : null }
</div>
})}
</Follow>
You can read about working with list in the new React docs
if you are refetching the follower and following list on every change it will be better to recalculate the followers list using a useMemo on every change
Hope this helps you in someway

why console gives in this sequence?

I am making a mern ecommerce website i just want to see how useEffect works so i console.log in some part of useEffect and loadFilteredResults i saw that --->
initial
entry
skip
entry1
screen shot
but i think it shoud be-->
initial
entry
entry1
skip
why console give this?? i am a begginer, i am a self learner , so please if you need any extra info please comment.
code snippet-->
const loadFilteredResults = (newFilters) => {
console.log("entry")
getFilteredProducts(skip, limit, newFilters).then((data) => {
console.log("entry1")
if (data.error) {
setError(data.error);
} else {
//console.log(data);
setFilteredResults(data.data);
//console.log("size-->");
//console.log(data.size);
setSize(data.size);
setSkip(0);
}
});
};
....
....
useEffect(() => {
init();
console.log("initial");
loadFilteredResults(skip, limit, myFilters.filters);
console.log("skip");
}, []);
//full code of shop.js
import React, { useEffect, useState } from "react";
import Layout from "./Layout";
import Card from "./Card";
import { getCategories, getFilteredProducts } from "./apiCore";
import Checkbox from "./Checkbox";
import RadioBox from "./RadioBox";
import { prices } from "./fixedPrices";
const Shop = () => {
const [myFilters, setMyFilters] = useState({
filters: { category: [], price: [] }
});
const [categories, setCategories] = useState([]);
const [error, setError] = useState(false);
const [limit, setLimit] = useState(3);//prpduct lesss so use 3 but sir used 6
const [skip, setSkip] = useState(0);
const [size, setSize] = useState(0);
const [filteredResults, setFilteredResults] = useState([]);
const init = () => {
getCategories().then((data) => {
if (data.error) {
//console.log("error");
setError(data.error);
} else {
//console.log("set");
//console.log(data);
setCategories(data);
//console.log(data);
}
});
};
const loadFilteredResults = (newFilters) => {
//console.log(newFilters);
console.log("entry")
getFilteredProducts(skip, limit, newFilters).then((data) => {
console.log("entry1")
if (data.error) {
setError(data.error);
} else {
//console.log(data);
setFilteredResults(data.data);
//console.log("size-->");
//console.log(data.size);
setSize(data.size);
setSkip(0);
}
});
};
const loadMore = () => {
console.log("skip"+skip);
console.log("limit"+limit);
let toSkip = skip + limit;
console.log("toSkip"+toSkip);
getFilteredProducts(toSkip, limit, myFilters.filters).then((data) => {
//console.log("filter");
//console.log( myFilters.filters)
if (data.error) {
setError(data.error);
} else {
//console.log(filteredResults);
//console.log(data.data);
setFilteredResults([...filteredResults, ...data.data]);
//console.log("after");
//console.log(...filteredResults);
//console.log(filteredResults);
//console.log(filteredResults);
//console.log([...filteredResults])
//console.log([...filteredResults, ...data.data])
setSize(data.size);
setSkip(toSkip);
}
});
};
const loadMoreButton = () => {
return (
size > 0 &&
size >= limit && (
<button onClick={loadMore} className="btn btn-warning mb-5">
load more
</button>
)
);
};
useEffect(() => {
init();
//console.log(skip);
console.log("initial");
loadFilteredResults(skip, limit, myFilters.filters);
console.log("skip");
}, []);
const handleFilters = (filters, filterBy) => {
//console.log("SHOP", filters, filterBy);
const newFilters = { ...myFilters };
//console.log(newFilters);
newFilters.filters[filterBy] = filters;
//console.log(typeof(filters));
if (filterBy === "price") {
let priceValues = handlePrice(filters);
newFilters.filters[filterBy] = priceValues;
//console.log(priceValues);
}
//console.log(myFilters.filters);
loadFilteredResults(myFilters.filters);
setMyFilters(newFilters);
};
const handlePrice = (value) => {
const data = prices;
let array = [];
//console.log(value);
for (let key in data) {
if (data[key]._id === parseInt(value)) {
array = data[key].array;
}
}
return array;
};
// const x = (filters)=>{
// console.log("filters:"+filters);
// handleFilters(filters, "category")
// }
return (
<Layout
title="Shop Page"
description="search and buy books of your choice"
className="container-fluid"
>
<div className="row">
<div className="col-4">
<h4>Filter by categories</h4>
<ul>
{/* below will be show in list show we wrap it in unorder list */}
<Checkbox
categories={categories}
handleFilters={(filters) =>
handleFilters(filters, "category")
}
/>
</ul>
<h4>Filter by price range</h4>
<div>
<RadioBox
prices={prices}
handleFilters={(filters) => handleFilters(filters, "price")}
/>
</div>
</div>
<div className="col-8">
<h2 className="mb-4">Products</h2>
<div className="row">
{filteredResults.map((product, i) => (
<Card key={i} product={product} />
))}
</div>
<hr />
{loadMoreButton()}
</div>
</div>
</Layout>
);
};
export default Shop;
getFilteredProducts must be a Promise. Please read Using promises
Callbacks added with then() will never be invoked before the
completion of the current run of the JavaScript event loop.

React listen to child's state from parent

Damn, two days, two noob questions, sorry guys.
Yesterday, I spent the whole afternoon reading the docs but my fart-ey brain cannot process how to use react hooks to pass data from a child to a parent.
I want to create a button on my parent that can listen to his child's state to check on it and change the background color depending on its value.
Thing is, the child component is mapping some stuff so I cannot create a button (otherwhise it would be rendered multiple times and not only once like I want).
I've thought about moving all the data to my parent component but I cannot understand how since I'm fairly new to React and it's been only two months of learning how to code for me basically.
I will now provide the code for the parent and the child component.
The parent :
import React from "react";
import Quizz from "./components/Quizz";
export default function App() {
const [quizz, setQuizz] = React.useState([]);
React.useEffect(() => {
async function getData() {
const res = await fetch(
"https://opentdb.com/api.php?amount=5&category=27&type=multiple"
);
const data = await res.json();
setQuizz(data.results)
}
getData();
}, []);
function checkOnChild(){ /* <== the function I'd like to use to check on my Quizz component's "activeAnswer" state */
console.log(quizz);
}
const cards = quizz.map((item, key) => {
return <Quizz {...item} key={key}/>;
});
return (
<div>
{cards}
<button onClick={checkOnChild}>Check answers</button> /* <== the button that will use the function */
</div>
);
}
and the child :
import React from "react";
import { useRef } from "react";
export default function Quizz(props) {
const [activeAnswer, setActiveAnswer] = React.useState('');/* <== the state I'd like to check on from my parent component */
function toggle(answer) {
setActiveAnswer(answer);
}
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
let temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
let answers = props.incorrect_answers;
const ref = useRef(false);
if (!ref.current) {
answers.push(props.correct_answer);
shuffleArray(answers);
ref.current = true;
}
const answerDiv = answers.map((answer, key) => (
<div key={key} className="individuals" onClick={()=> toggle(answer)}
style={{background: answer == activeAnswer ? "#D6DBF5" : "transparent" }}>
{answer}
</div>
));
console.log(answers);
console.log(activeAnswer);
console.log(props.correct_answer);
return (
<div className="questions">
<div>
<h2>{props.question}</h2>
</div>
<div className="individuals__container">{answerDiv}</div>
<hr />
</div>
);
}
I'm really sorry If it seems dumb or if I'm making forbidden things lmao, but thanks in advance for your help guys!
This should get you a bit further I think.
export default function App() {
const [quizData, setQuizData] = useState([])
const [quizState, setQuizState] = useState({})
useEffect(() => {
async function getData() {
const res = await fetch('https://opentdb.com/api.php?amount=5&category=27&type=multiple')
const data = await res.json()
const results = data.results
setQuizData(results)
setQuizState(results.reduce((acc, curr) => ({ ...acc, [curr.question]: '' }), {}))
}
getData()
}, [])
function checkOnChild() {
console.log(quizState)
}
const cards = quizData.map((item) => {
return <Quizz {...item} key={item.question} quizState={quizState} setQuizState={setQuizState} />
})
return (
<div>
{cards}
<button onClick={checkOnChild}>Check answers</button>
</div>
)
}
export default function Quizz(props) {
function handleOnClick(answer) {
props.setQuizState(prevState => ({
...prevState,
[props.question]: answer,
}))
}
const answers = useMemo(() => {
const arr = [...props.incorrect_answers, props.correct_answer]
return shuffleArray(arr)
}, [props.incorrect_answers, props.correct_answer])
const answerDiv = answers.map((answer) => (
<div
className="individuals"
key={answer}
onClick={() => handleOnClick(answer)}
style={{ background: answer == props.quizState[props.question] ? '#D6DBF5' : 'transparent' }}
>
{answer}
</div>
))
return (
<div className="questions">
<div>
<h2>{props.question}</h2>
</div>
<div className="individuals__container">{answerDiv}</div>
<hr />
</div>
)
}

React - autocomplete search for API - array always returns empty

I'm trying to make an autocomplete search for Alpha Vantage API, but the array which should contain the matches (suggestion) always returns empty when I type in the input field and I don't know what could be the reason, I'm stuck for a while on this and would appreciate if someone could help me with this.
The related code here is mostly in useEffect and the inputHandler:
const Search = () => {
useEffect(() => {
const getSymbols = async () => {
const searchURL = `https://www.alphavantage.co/query?function=SYMBOL_SEARCH&keywords=${textInput}&apikey=${process.env.REACT_APP_ALPHA_VANTAGE_API_KEY}`
const res = await axios.get(searchURL);
if(res) {
setSecurity(res.data.bestMatches);
if(security !== undefined && security.length > 0) {
let symbols = security.map(sec => sec['1. symbol'])
setAllSymbol(symbols);
}
}
}
getSymbols();
}, [])
console.log(allSymbol);
const inputHandler = (e) => {
setTextInput(e.target.value);
let matches = [];
if(textInput.length > 0) {
matches = allSymbol.filter(sym => {
const regex = new RegExp(`${textInput}`, "gi");
return sym.match(regex);
})
setSuggestion(matches);
}
console.log(suggestion);
setTextInput(e.target.value);
}
const showData = async (e) => {
e.preventDefault();
const searchURL = `https://www.alphavantage.co/query?function=SYMBOL_SEARCH&keywords=${textInput}&apikey=${process.env.REACT_APP_ALPHA_VANTAGE_API_KEY}`
const monthlyURL = `https://www.alphavantage.co/query?function=TIME_SERIES_MONTHLY&symbol=${textInput}&apikey=${process.env.REACT_APP_ALPHA_VANTAGE_API_KEY}`
try {
const res = await axios.get(searchURL);
const data = await axios.get(monthlyURL);
if(res) {
setTickers(res.data.bestMatches[0]);
setSymbol(res.data.bestMatches[0]['1. symbol']);
setSecurity(res.data.bestMatches);
if(data) {
const monthlyTimeSeries = Object.values(data.data['Monthly Time Series']);
const result = [monthlyTimeSeries[1]];
const resultValues = Object.keys(result[0]).map(key => {
return Math.floor(result[0][key]);
})
setPrices(resultValues);
}
}
} catch(err) {
console.log(err)
}
setDailyPrices([]);
setWeeklyPrices([]);
setIntraPrices([]);
}
return (
<StyledSearch>
<div className="wrapper">
<h1>Security Price Monitor App </h1>
<form onSubmit={showData} className="search-form">
<input type="text" value={textInput} onChange={inputHandler} placeholder='Enter Stock Symbol (GOOG, MSFT)'/>
<button type="submit">Search</button>
</form>
</div>
{prices.length > 0 && (
<>
<Table prices={prices} symbol={symbol}/>
<TimeFrames symbol={symbol} textInput={textInput} weeklyPrices={weeklyPrices} setWeeklyPrices={setWeeklyPrices} dailyPrices={dailyPrices} setDailyPrices={setDailyPrices} intraPrices={intraPrices} setIntraPrices={setIntraPrices} />
</>
)}
</StyledSearch>
)
}

React button to show an element

I'm pulling countries from the Restcountries API and if the current state of the array has more than one or less than or equal to ten countries, I want to list the country names along with a 'show' button next to each one. The show button should display what's in the return (render) of my Country function. In the App function, I wrote a handler for the button named handleViewButton. I'm confused on how to filter the element in the Countries function in the else conditional statement in order to display the Country. I tried passing handleViewButton to the Button function, but I get an error 'Uncaught TypeError: newSearch.toLowerCase is not a function'. I really just want to fire the Country function to display the country button that was pressed.
App.js
import React, { useState, useEffect } from 'react';
import './App.css';
import axios from 'axios';
const Country = ({country}) => {
return (
<>
<h2>{country.name}</h2>
<p>capital {country.capital}</p>
<p>population {country.population}</p>
<br/>
<h3>languages</h3>
{country.languages.map(language => <li key={language.name}>{language.name}</li>)}
<br/>
<img src={country.flag} alt="country flag" style={{ width: '250px'}}/>
</>
);
}
const Countries = ({countries, handleViewButton}) => {
const countriesLen = countries.length;
console.log(countriesLen)
if (countriesLen === 0) {
return (
<p>Please try another search...</p>
);
} else if (countriesLen === 1) {
return (
<ul>
{countries.map((country, i) => <Country key={i} countriesLen={countriesLen} country={country}/>)}
</ul>
);
} else if (countriesLen > 10) {
return (
<div>
<p>Too many matches, specify another filter</p>
</div>
);
} else {
return (
<ul>
{countries.map((country, i) => <li key={i}>{country.name}<Button handleViewButton={handleViewButton}/></li>)}
</ul>
)
};
};
const Button = ({handleViewButton}) => {
return (
<button onClick={handleViewButton}>Show</button>
);
};
const Input = ({newSearch, handleSearch}) => {
return (
<div>
find countries <input value={newSearch} onChange={handleSearch}/>
</div>
);
};
function App() {
const [countries, setCountries] = useState([]);
const [newSearch, setNewSearch] = useState('');
const handleSearch = (event) => {
const search = event.target.value;
setNewSearch(search);
};
const handleViewButton = (event) => {
const search = event.target.value;
setNewSearch(countries.filter(country => country === search));
};
const showCountrySearch = newSearch
? countries.filter(country => country.name.toLowerCase().includes(newSearch.toLowerCase()))
: countries;
useEffect(() => {
axios
.get('https://restcountries.eu/rest/v2/all')
.then(res => {
setCountries(res.data);
console.log('Countries array loaded');
})
.catch(error => {
console.log('Error: ', error);
})
}, []);
return (
<div>
<Input newSearch={newSearch} handleSearch={handleSearch}/>
<Countries countries={showCountrySearch} handleViewButton={handleViewButton}/>
</div>
);
};
export default App;
you can use a displayCountry to handle the country that should be displayed. Most often you would use an id, but I'm using here country.name since it should be unique.
Then you would use matchedCountry to find against your list of countries.
After that, a onHandleSelectCountry to select a given country. if it's already selected then you could set to null to unselect.
Finally, you would render conditionally your matchedCountry:
const Countries = ({countries}) => {
const [displayCountry, setDisplayCountry] = useState(null);
const countriesLen = countries.length;
const matchedCountry = countries.find(({ name }) => name === displayCountry);
const onHandleSelectCountry = (country) => {
setDisplayCountry(selected => {
return selected !== country.name ? country.name : null
})
}
if (countriesLen === 0) {
return (
<p>Please try another search...</p>
);
} else if (countriesLen === 1) {
return (
<ul>
{countries.map((country, i) => <Country key={i} countriesLen={countriesLen} country={country}/>)}
</ul>
);
} else if (countriesLen > 10) {
return (
<div>
<p>Too many matches, specify another filter</p>
</div>
);
} else {
return (
<>
<ul>
{countries.map((country, i) => <li key={i}>{country.name}<Button handleViewButton={() => onHandleSelectCountry(country)}/></li>)}
</ul>
{ matchedCountry && <Country countriesLen={countriesLen} country={matchedCountry}/> }
</>
)
};
};
I can only help to point out some guidelines.
First: The button does not have value attribute. Hence what you will get from event.target.value is always blank.
const Button = ({handleViewButton}) => {
return (
<button onClick={handleViewButton}>Show</button>
);};
First->Suggestion: Add value to the button, of course you need to pass the value in.
const Button = ({handleViewButton, value}) => {
return (
<button onClick={handleViewButton} value={value}>Show</button>
);};
Second: To your problem 'Uncaught TypeError: newSearch.toLowerCase is not a function'. Filter always returns an array, not a single value. if you do with console or some sandbox [1,2,3].filter(x=>x===2) you will get [2] not 2.
const handleViewButton = (event) => {
const search = event.target.value;
setNewSearch(countries.filter(country => country === search));
};
Second->Suggestion: To change it to get the first element in array, since country(logically) is unique.
const result = countries.filter(country => country === search)
setNewSearch(result.length>0?result[0]:"");
A better approach for array is find, which always return first result and as a value. E.g. [1,2,2,3].find(x=>x===2) you will get 2 not [2,2] or [2].
countries.find(country => country === search)

Resources