Map and If statement unexpected behaviour - reactjs

The results I want to achieve are:
If the entered name is already on the array "persons", show an alert "Name already exist".
The code is not working, because I keep on getting the alert message all the time.
I think the condition comparison variable (personObject.name ) is wrong.
Can someone explain how this should be done?
Without the condition, the names ad added propperly into the array.
//Condition
if (persons.map((person) => person.name === personObject.name) )
{
alert("Name already exist");
}
else
{
//*/
setPersons(persons.concat(personObject))
setNewName('')
console.log(persons)
}
Code sandbox
Full code:
const App = () => {
//Reminder: current state, function that updates it, initial state.
const [persons, setPersons] = useState([
//The array persons is empty at start
]);
const [newName, setNewName] = useState('');
//adding new persons
const addPerson = (event) => {
event.preventDefault();
/* complete the addPerson function for creating new persons */
const personObject = {
name: newName,
id: persons.length + 1,
};
//Condition
if (persons.map((person) => person.name === personObject.name)) {
alert('Name already exist');
} else {
//*/
setPersons(persons.concat(personObject));
setNewName('');
console.log(persons);
}
};
const handlePersonChange = (event) => {
console.log(event.target.value);
setNewName(event.target.value);
};
return (
<div>
<h2>Phonebook</h2>
<form onSubmit={addPerson}>
<div>
name:
<input value={newName} onChange={handlePersonChange} />
</div>
<div>
<button type="submit">add</button>
</div>
</form>
<h2>Numbers</h2>
{console.log(persons)}
<ul>
{persons.map((person) => (
<li key={person.id}>{person.name}</li>
))}
</ul>
</div>
);
};
export default App;

You are checking condition with using map and map will return new array so if condition will be always true.
So instead of map you should use some like below:-
persons.some((person) => person.name === personObject.name);

You can use the filter method that filters an array and returns you an array with items matching the condition, if this new array is empty it means no items matches it :
let alreadyExists = persons.filter(person => person.name === personObject.name)
if (alreadyExists.length > 0) {
alert("Name already exist")
} else {
setPersons(persons.concat(personObject))
setNewName('')
console.log(persons)
}

Related

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)

Cannot remove inputs array with filter

I am trying to remove an input field with filter function but it's not working.
In the following code add operation works fine but remove operation is not working properly ,it is not removing the corresponding element.Another problem the values on the inputs fields not present when the component re-renders.so experts guide me how i can achieve removing the corresponding row when the remove button is clicked and the input values should not be reset when the component re-renders
So when I refresh the page and click to remove an input it will clear all other input data. How can I fix this problem ?
Update adding full component in question:
const Agreement = (props) => {
const { agreement, editable, teamData, teamId, fetchTeamData } = props;
const [editing, setEditing] = useState(false);
const [title, setTitle] = useState("");
const [showErrors, setShowErrors] = useState(false);
const [errorsArr, setErrorsArr] = useState();
const initialFormState = {
rule_0: teamData.rules.rule_0,
rule_1: teamData.rules.rule_1,
rule_2: teamData.rules.rule_2,
rule_3: teamData.rules.rule_3,
creator: teamData.User.public_user_id,
};
const [updateTeamData, setUpdateTeamData] = useState(initialFormState);
const [inputs, setInputs] = useState(
teamData.rules.map((el) => ({
...el,
guid: uuidV4(),
}))
);
const handleChange = (event) => {
const { name, value } = event.target;
// Update state
setUpdateTeamData((prevState) => ({
...prevState,
[name]: value,
}));
};
// Add more input
const addInputs = () => {
setInputs([...inputs, { name: `rule_${inputs.length + 1}` }]);
};
// handle click event of the Remove button
const removeInputs = (index) => {
const newList = inputs.filter((item, i) => index !== i); // <-- compare for matching index
setInputs(newList);
};
const clearInput = (dataName) => {
setUpdateTeamData((prevState) => {
delete prevState[dataName];
return {
...prevState,
};
});
};
const handleSubmit = async (event) => {
event.preventDefault();
setEditing(false);
// Send update request
const res = await axios.put(`/api/v1/teams/team/${teamId}`, updateTeamData);
// If no validation errors were found
// Validation errors don't throw errors, it returns an array to display.
if (res.data.validationErrors === undefined) {
// Clear any errors
setErrorsArr([]);
// Hide the errors component
setShowErrors(false);
// Call update profiles on parent
fetchTeamData();
} else {
// Set errors
setErrorsArr(res.data.validationErrors.errors);
// Show the errors component
setShowErrors(true);
}
};
const handleCancel = () => {
setEditing(false);
};
useEffect(() => {
if (agreement === "default") {
setTitle(defaultTitle);
// setInputs(teamData.rules);
} else {
setTitle(agreement.title ?? "");
}
}, [agreement, teamData]);
// console.log("teamData.rules", teamData);
console.log("inputs", inputs);
return (
<div className="team-agreement-container">
{!editing && (
<>
<h4 className="team-agreement-rules-title">{title}</h4>
{editable && (
<div className="team-agreement-rules">
<EditOutlined
className="team-agreement-rules-edit-icon"
onClick={() => setEditing(true)}
/>
</div>
)}
{teamData.rules.map((rule, index) => (
<div className="team-agreement-rule-item" key={`rule-${index}`}>
{rule ? (
<div>
<h4 className="team-agreement-rule-item-title">
{`Rule #${index + 1}`}
</h4>
<p className="team-agreement-rule-item-description">
- {rule}
</p>
</div>
) : (
""
)}
</div>
))}
</>
)}
{/* Edit rules form */}
{editing && (
<div className="team-agreement-form">
{showErrors && <ModalErrorHandler errorsArr={errorsArr} />}
<h1>Rules</h1>
{inputs.map((data, idx) => {
return (
<div className="agreement-form-grid" key={data.guid}>
<button
type="button"
className="agreement-remove-button"
onClick={() => {
removeInputs(idx);
clearInput(`rule_${idx}`);
}}
>
<Remove />
</button>
<input
name={`rule_${idx}`}
onChange={handleChange}
value={teamData.rules[idx]}
/>
</div>
);
})}
{inputs.length < 4 && (
<div className="team-agreement-add-rule">
<button type="submit" onClick={addInputs}>
<Add />
</button>
</div>
)}
<div className="div-button">
<button className="save-button" onClick={handleSubmit}>
Save
</button>
<button className="cancel-button" onClick={handleCancel}>
Cancel
</button>
</div>
</div>
)}
</div>
);
};
export default Agreement;
When i do console.log(inputs) this is the data that I got:
0: 0: "t" 1: "e" 2: "s" guid: "e18595a5-e30b-4b71-8fc2-0ad9c0e140b2"
proto: Object 1: 0: "d" 1: "a" 2: "s" 3: "d" 4: "a" 5: "s" guid: "537ca359-511b-4bc6-9583-553ea6ebf544" ...
Issue
The issue here is that you are using the array index as the React key. When you mutate the underlying data and reorder or add/remove elements in the middle of the array then the elements shift around but the React key previously used doesn't move with the elements.
When you remove an element then all posterior elements shift forward and the index, as key, remains the same so React bails on rerendering the elements. The array will be one element shorter in length and so you'll see the last item removed instead of the one you actually removed.
Solution
Use a React key that is intrinsic to the elements being mapped, unique properties like guids, ids, name, etc... any property of the element that guarantees sufficient uniqueness among the dataset (i.e. the siblings).
const [inputs, setInputs] = useState(teamData.rules);
const removeInputs = (index) => {
// compare for matching index
setInputs(inputs => inputs.filter((item, i) => index !== i));
};
{inputs.map((data, idx) => {
return (
<div className="agreement-form-grid" key={data.id}> // <-- use a unique property
<button
type="button"
className="agreement-remove-button"
onClick={() => {
removeInputs(idx);
clearInput(`rule_${idx}`);
}}
>
<Remove />
</button>
<input
name={`rule_${idx}`}
onChange={handleChange}
value={teamData.rules[idx]}
/>
</div>
);
})}
If your teamData.rules initial state value doesn't have any unique properties to use then you can map this to a new array and add a sufficient id property.
const [inputs, setInputs] = useState(teamData.rules.map(el => ({
...el,
guid: generateId()***,
})));
*** this is a function you need to define yourself, or import from a module like uuid***
import { v4 as uuidV4 } from 'uuid';
...
const [inputs, setInputs] = useState(teamData.rules.map(el => ({
...el,
guid: uuidV4(),
})));
// Add more input
const addInputs = () => {
setInputs(inputs => [
...inputs,
{
name: `rule_${inputs.length + 1}`,
guid: uuidV4();
},
]);
};
Then when mapping use the guid property.
<div className="agreement-form-grid" key={data.guid}>
The issue is because you are trying to compare index with array item in filter method. You should use the second argument in filter which denotes the array index of the current iterating item
const removeInputs = (index) => {
const newList = inputs.filter((item,i) => index !== i);
setInputs(newList);
};
That's your solution, you are trying with item but you are comparing it with index that's wrong. You should do it like this,
const newList = inputs.filter((item, key) => index !== key);

Toggle button inside map

I'm trying to set a toggle inside a map , it activates with a button , now i handled to difference between every single item of the card but there's one problem, since the state changes, i mean i'm not updating it, the state doesn't let me have several buttons activated, i tried making the initial state an array of objects but i can't upload one single position of the array...
here's the code:
Handler
//hooks
const [detail, setDetail] = useState([{
id: '',
state: false,
}]);
const handleClick = (e, id) => {
setDetail({ ...detail, id: id, state: !detail['state'] })
}
Map
<div className={Styles.wrapper}>
{
artistSales && artistSales.map(s => {
return (
<div className={Styles.align}>
<div className={Styles.card}>
<div className={Styles.order}>
<h2>Numero de orden: {s.id_order}</h2>
<p>Estado de pago: {s.state === 'fullfilled' && 'pago realizado'} </p>
<p>Cliente: {s.userId.name} {s.userId.lastname} </p>
<p>Email:
<a href={`mailto:${s.userId.email}`}>{s.userId.email}</a>
</p>
</div>
<div className={Styles.detail}>
<p>Total: ${s.total_price}</p>
<p>Fecha: {s.createdAt.slice(0, 10)}</p>
<button value={s.id_order} onClick={(e) => handleClick(e, s.id_order)} className={Styles.btn}>Ver detalles</button>
</div>
</div>
{detail.id === s.id_order && detail.state === true && <div>
hello
</div>}----> this is what should be displayed when you click the button
you need to first find a matching id. if there is none you should add it to array, otherwise make a copy from the match with new state value.
you can create also a function to check if for a given id that specific state value to render your content logic:
const [details, setDetails] = useState([]);
const handleClick = (e, id) => {
setDetails(details => {
// find index for matching detail
const detailIndex = details.findIndex(d => d.id === id)
// no match return new state
if (detailIndex === -1) {
return [...details, { id, state: true }]
}
// with match create copy details
const nextDetails = [...details]
// create copy of detail with a flipped state value
const state = !nextDetails[detailIndex].state
nextDetails[detailIndex] = { ...nextDetails[detailIndex], state }
return nextDetails
})
}
// function to get artist detail state
const detailForArtist = ( id ) => {
const detail = details.find(d => d.id === id)
return detail?.state
}
{ detailForArtist(s.id_order) && <div>hello</div> }

How to set the first value in a select dropdown to as a default react

I have a react program that displays a table based on values of a dropdown. I want the program to display the table by default based on the first value in the dropdown.
The first value in the dropdown is very different from the value made as default, and the dropdown values are always changing. So the data can be misleading when it loads for the first time.
here is a snippet of my code with a little description within. Thanks.
const CompletenessApp = () => {
const [completeness, setcompleteness] = useState([]);
const [loading, setloading] = useState(false);
const [valueSelected, setValueSelected] = useState({value:'all'});
const [periodSelected, setPeriodSelected] = useState({value:'20200702'}); // the default value that is used to filter the data.
const valid = [
{id:"all", variable:"all"},{id:"true", variable:"true"}, {id:"false", variable:"false"}, {id:"null", variable:"null"},
];
useEffect(() => {
const fetchData = async () => {
try{
const res = await axios.get('http://localhost:8000/api/completeness/');
setcompleteness(res.data);
setloading(true);
} catch (e) {
console.log(e)
}
}
fetchData();
}, []);
// when the page loads for the first time it filters the data based on the period value set in the state. I want the data to be filtered based on the first value in the dropdown instead, the first value in the dropdown is different from the default value set.
const handlePeriodChange = e => {
setPeriodSelected({value : e.target.value})
}
const handleValueChange = e => {
let boolvalue = Boolean
e.target.value === 'true'? boolvalue = true:
e.target.value === 'false'? boolvalue = false:
e.target.value === 'all'? boolvalue = 'all':
boolvalue=null
setValueSelected({value : boolvalue})
}
//filtered data to be displayed
const filteredCompleteness = completeness.filter(
completedata=> (completedata.period === periodSelected.value)
&&(valueSelected.value !== 'all'?completedata.complete === valueSelected.value:{}))
return(
<div>
<div className="selection-row">
<div className="stats-columns">
<div className="stats-label">Period</div>
//the relevant dropdown is here
<select onChange={e => handlePeriodChange(e)}>
{Array.from(new Set(completeness.map(obj => obj.period)))
.sort().reverse()
.map(period => {
return <option value={period}>{period}</option>
})}
</select>
</div>
<div className="stats-columns">
<div className="stats-label">Value</div>
<select onChange={e => handleValueChange(e)}>
{valid.map((obj) => {
return <option value={obj.id}>{obj.variable}</option>
})
}
</select>
</div>
</div>
<hr></hr>
<div className="results-table">
//table is displayed here
</div>
</div>
);
}
export default CompletenessApp;
// above return
const options = Array.from(new Set(completeness.map((obj) => obj.period)))
.sort()
.reverse()
.map((period) => {
return {
value: period,
label: period,
};
});
// in jsx
<select defaultValue={options[0].value} onChange={e => handlePeriodChange(e)}>
{
options.map((obj) => {
return <option value={obj.value}>{obj.label}</option>;
})
}
</select>
Try this and let me know.

Search Filter input ReactJS

I have class Comopnent :
state = {
names: ['first', 'second']
};
updateSearch = (event) => {
let updatedList = this.state.names;
updatedList = updatedList.filter(name=> {
return name.toLowerCase().search(
event.target.value.toLowerCase()) !== -1;
})
this.setState({names: updatedList});
}
render() {
const names = this.state.names.map((name) => { return (
<div key={name}>
<span>{name}</span>
</div>
)})
return (
<div>
<input
placeholder="Search"
type="text"
onChange={this.updateSearch} />
{names}
</div>
)
}
When I type some text that agrees with the name, search is working, and only that name is showing, but when i remove text from input all names should show back, but they don't (only the previously searched name is displayed). Why?
Thanks for answers in advance!
Add one more val in state as initialName and in updateSearch set updateSearch value
with initalNames.
Try This.
state = {
names: ['first', 'second'],
intiailNames:['first','second']
};
updateSearch = (event) => {
let updatedList = this.state.intiailNames;
updatedList = updatedList.filter(name=> {
return name.toLowerCase().search(
event.target.value.toLowerCase()) !== -1;
})
this.setState({names: updatedList});
}

Resources