I am trying to refactor my code into components. So right now I want to refactor the AddName component. This AddName component is to be pass to an event handler <form>(below the header Add Info). The problem right now is that now after refactoring the code, exporting and importing it and passing it to the event handler, I got an error in my VS code saying setPersons is assigned but never used. This is not something that I expect it to be since the other props in the AddName component have no problems but only this setPersons. I only know that to pass prop in between components, usually one will do it like this,
<AddName setPersons={setPersons}/>
In this situation it is not appropiate to use something like this right ? How can I fix this ? Below are my code,
App.js
import { useState } from 'react'
import AddName from './components/AddName'
const App = () => {
const [persons, setPersons] = useState([
{ name: 'Arto Hellas', number: '012-3456789', id: 1 },
{ name: 'Ada Lovelace', number: '013-4567890', id: 2 },
{ name: 'Dan Abramov', number: '011-1234567', id: 3 },
{ name: 'Mary Poppendieck', number: '016-5556789', id: 4 }
])
const [newName, setNewName] = useState('')
const [newNumber, setNewNumber] = useState('')
const [searchTerm, setSearchTerm] = useState("")
const handleNameChange = (event) => {
setNewName(event.target.value)
}
const handleNumberChange = (event) => {
setNewNumber(event.target.value)
}
return (
<div>
<h2> Phonebook </h2>
<input type="text" placeholder="Search..." onChange= {(event) => setSearchTerm(event.target.value)} />
<h2> Add Info </h2>
<form onSubmit={AddName}>
<div>
name: <input value={newName} onChange={handleNameChange} />
</div>
<div>
phonenumber: <input value={newNumber} onChange={handleNumberChange} />
</div>
<div>
<button type="submit"> add </button>
</div>
</form>
<h2> Numbers </h2>
<ul>
{persons.filter((person) => {
if (searchTerm === "") {
return person
} else if (person.name.toLowerCase().includes(searchTerm.toLowerCase())) {
return person
}
}).map((person) => {
return (
<li key={person.name}> {person.name} {person.number} </li>
);
})
}
</ul>
</div>
)
}
export default App
AddName.js
const AddName = (props, event) => {
event.preventDefault()
const nameObject = {
name: props.newName,
number: props.newNumber2
}
const isNameExist = (value) => props.persons.some(person => person.name.includes(value))
if (isNameExist(nameObject.name)) {
alert("name already exist")
} else {
props.setPersons(props.persons.concat(nameObject))
props.setNewName('')
props.setNewNumber('')
}
}
export default AddName
Why whould you pass a component to an onSubmit handler? Do you expect anything to render? I would have a simple function in the App component like this:
const addName = (event) => {
event.preventDefault()
const nameObject = {
name: newName,
number: newNumber2
}
const isNameExist = (value) => persons.some(person => person.name.includes(value))
if (isNameExist(nameObject.name)) {
alert("name already exist")
} else {
setPersons(persons.concat(nameObject))
setNewName('')
setNewNumber('')
}
}
...
...
<form onSubmit={addName}>
...
I would also extract the list in another component otherwise also the list would rerender at all the state changes (i.e. at all input change)
Related
so I'm a beginner and I'm trying to build a simple phonebook, where you can add persons and filter them with their name.
My problem is, that when I'm trying to add a new contact, the new contact does't show until I write something in the searchbar.
When I deleted the search component, I was able to add contacts normally.
Here is my code so far:
(App.js)
import { useState } from 'react'
import Person from './components/Person'
const App = () => {
const [persons, setPersons] = useState([
{ name: 'Arto Hellas', number: '040-123456', id: 1 },
{ name: 'Ada Lovelace', number: '39-44-5323523', id: 2 },
{ name: 'Dan Abramov', number: '12-43-234345', id: 3 },
{ name: 'Mary Poppendieck', number: '39-23-6423122', id: 4 }
])
const [newName, setNewName] = useState('')
const [newNumber, setNewNumber] = useState('')
const[newSearch, setNewSearch] = useState('')
const[personsFilter, setPersonsFilter] = useState(persons)
const addContact = (event) => {
event.preventDefault()
const nameObject = {
name: newName,
number: newNumber,
id: persons.length+1,
}
const currentPerson = persons.filter((person) => person.name === newName);
if (currentPerson.length === 1) {
alert(`${newName} is already added to phonebook`)
} else {
setPersons(persons.concat(nameObject))
setNewName('')
setNewNumber('')
}
}
const handleNameChange = (event) => {
setNewName(event.target.value)
}
const handleNumberChange = (event) => {
setNewNumber(event.target.value)
}
const filterPersons = (event) => {
const searchName = event.target.value.toLowerCase()
setNewSearch(searchName)
const newPersons = persons.filter (
(person) =>
person.name.toLowerCase().search(searchName) !== -1
)
setPersonsFilter(newPersons)
}
return (
<div>
<h2>Phonebook</h2>
<div>
filter:
<input value={newSearch}
onChange={filterPersons}
/>
</div>
<form onSubmit={addContact}>
<div>
name: <input
value={newName}
onChange={handleNameChange}
/>
</div>
<div>
number: <input
value={newNumber}
onChange={handleNumberChange}/>
</div>
<div>
<button type="submit">add</button>
</div>
</form>
<h2>Numbers</h2>
<ul>
<Person persons={personsFilter} />
</ul>
</div>
)
}
export default App
(Persons.js)
import React from "react";
const Person = ({ persons }) => {
return persons.map((person) =>
<li key={person.id}>{person.name}: <span>{person.number}</span></li>
)
}
export default Person
and index.js
import React from 'react';
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(<App />, document.getElementById('root'))
I hope I explained my error clearly lol
Thanks in advance
Have great day <3
The problem with your code is when you set a new person it only updates the persons state not the personsFilter state. The personsFilter state is the one being used to show data to the DOM. The reason it works when you filter is because your function filterPersons() takes a copy of the persons state.
Instead of focusing on two states why not just use a single searchTerm state then just filter that when you map?
This is how I would do it:
App.js
import { useState } from 'react'
import Person from './components/Person'
const App = () => {
const [persons, setPersons] = useState([
{ name: 'Arto Hellas', number: '040-123456', id: 1 },
{ name: 'Ada Lovelace', number: '39-44-5323523', id: 2 },
{ name: 'Dan Abramov', number: '12-43-234345', id: 3 },
{ name: 'Mary Poppendieck', number: '39-23-6423122', id: 4 }
])
const [newName, setNewName] = useState('')
const [newNumber, setNewNumber] = useState('')
const[searchTerm, setSearchTerm] = useState('')
const addContact = (event) => {
event.preventDefault()
const nameObject = {
name: newName,
number: newNumber,
id: persons.length+1,
}
const currentPerson = persons.filter((person) => person.name === newName);
if (currentPerson.length === 1) {
alert(`${newName} is already added to phonebook`)
} else {
setPersons(persons.concat(nameObject))
setNewName('')
setNewNumber('')
}
}
const handleNameChange = (event) => {
setNewName(event.target.value)
}
const handleNumberChange = (event) => {
setNewNumber(event.target.value)
}
const filterPersons = (event) => {
setSearchTerm(event.target.value.toLowerCase())
}
return (
<div>
<h2>Phonebook</h2>
<div>
filter:
<input value={searchTerm}
onChange={filterPersons}
/>
</div>
<form onSubmit={addContact}>
<div>
name: <input
value={newName}
onChange={handleNameChange}
/>
</div>
<div>
number: <input
value={newNumber}
onChange={handleNumberChange}/>
</div>
<div>
<button type="submit">add</button>
</div>
</form>
<h2>Numbers</h2>
<ul>
<Person persons={persons} searchTerm={searchTerm} />
</ul>
</div>
)
}
export default App
Person.js
const Person = ({ persons, searchTerm }) => {
return persons
.filter((person) => person.name.toLowerCase().includes(searchTerm))
.map((person) => (
<li key={person.id}>
{person.name}: <span>{person.number}</span>
</li>
));
};
export default Person;
https://codesandbox.io/s/silly-architecture-iu8qkz?file=/src/App.js
This question already has answers here:
Correct modification of state arrays in React.js
(19 answers)
Closed 1 year ago.
how i can add more arrays in use state when i add a array by using a setAddtaskv array will be add but format is not true
import react,{useState,useEffect} from 'react'
const App = () => {
const [addtaskV,setAddtaskv] = useState([
{ id: 1, title: "Alabama",text:"Test" },
{ id: 2, title: "Georgia",text:"Test" },
{ id: 3, title: "Tennessee",text:"Test" }
]);
const addTask = () =>
{
const title = document.getElementById('title').value;
const text = document.getElementById('text').value;
}
return (
<div>
<input type="text" placeholder="Title" id="title"/>
<input type="text" placeholder="Write some text" id="text" />
<button onClick={addTask}>Add</button>
</div>
);`enter code here`
}
export default App;
On your addTask you could do this
const addTask = () => {
const title = document.getElementById("title").value;
const text = document.getElementById("text").value;
setAddtaskv((previousState) => [
...previousState,
{ id: addtaskV.length + 1, title: title, text: text }
]);
};
The spread on ...previousState means that you get whatever is already inside there and you add your new object to it.
Here's the full component
import react, { useState, useEffect } from "react";
const App = () => {
const [addtaskV, setAddtaskv] = useState([
{ id: 1, title: "Alabama", text: "Test" },
{ id: 2, title: "Georgia", text: "Test" },
{ id: 3, title: "Tennessee", text: "Test" }
]);
const addTask = () => {
const title = document.getElementById("title").value;
const text = document.getElementById("text").value;
setAddtaskv((previousState) => [
...previousState,
{ id: addtaskV.length + 1, title: title, text: text }
]);
};
return (
<>
<div>
<input type="text" placeholder="Title" id="title" />
<input type="text" placeholder="Write some text" id="text" />
<button onClick={addTask}>Add</button>
</div>
{addtaskV.map((task) => (
<div>
<span>
{task.id} - {task.title} - {task.text}
</span>
</div>
))}
</>
);
};
export default App;
EDIT: I also suggest that you do some research on how to properly develop React forms. Although your solution works, there are better ways of doing it. But you seem to be doing well 🤘
This is the general and correct way to do this in React JS
const [item, setItem] = useState({title: '', text: '', id: 0})
const [data, setData] = useState([])
// get inputs values
const handleInput = ({name, value}) => {
setItem({...item, [name]:value, id:data.length})
}
// new item object to `data`
const submit = () => {
if(item.title === '' || item.text === ''){
console.log('Please, fill empty spaces')
}else{
setData({...data, item})
}
}
<input value={item.title} name='title'
onChange={(e)=>handleInput(e.target)} />
<input value={item.text} name='text'
onChange={(e)=>handleInput(e.target)} />
<button onclick={()=>submit()}>Add new</button>
// render to screen
data &&
data.map(({title, text, id}) => <div key={id}>
<h1>{title}</h1>
<p>{text}</p>
</div>)
Please, do not use DOM Manipulation in React if not crucial
I have this error that says everytime I'll submit it:
TypeError: s.indexOf is not a function
import React, { useState } from "react";
const checkboxes = [
{ id: 1, text: "Checkbox 1" },
{ id: 2, text: "Checkbox 2" },
{ id: 3, text: "Checkbox 3" }
];
const SearchResults = () => {
const [selectedCheckbox, setSelectedCheckbox] = useState([]);
const handleChange = (id) => {
const findIdx = selectedCheckbox.indexOf(id);
if (findIdx > -1) {
selectedCheckbox.splice(findIdx, 1);
} else {
selectedCheckbox.push(id);
}
setSelectedCheckbox(selectedCheckbox);
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(JSON.stringify(selectedCheckbox));
try {
const userRef = db.collection("users").doc(uid);
const ref = userRef.set({
selectedCheckbox
});
console.log("done")
} catch (err) {
console.log(err);
}
};
return (
<div>
<div className="App">
<form onSubmit={handleSubmit}>
{checkboxes.map((checkbox) => (
<label key={checkbox.id}>
{checkbox.text}
<input
type="checkbox"
onChange={() => handleChange(checkbox.text)}
selected={selectedCheckbox.includes(checkbox.id)}
/>
</label>
))}
<button type="submit">Submit</button>
</form>
</div>
<p>Selected checkboxes: {JSON.stringify(selectedCheckbox)}</p>
</div>
);
};
This is the codesandbox, though there's not Firestore data here:
https://codesandbox.io/s/handle-multiple-checkboxes-in-react-forked-xehxf?file=/src/App.js:0-2520
Any help would be appreciated. Thank you.
I suspect the error rises when your tracking array of selectedCheckboxes goes undefined even though this is not quite the case in the linked code sandbox.
In React as a general rule, you should never mutate the state even using React hooks which is you case in the handleChange method.
You should update you event handler method as follows (always making a copy of the state array to construct a the new one):
const handleChange = (id) => {
const findIdx = selectedCheckbox.indexOf(id);
let selected;
if (findIdx > -1) {
selected = selectedCheckbox.filter((checkboxId) => checkboxId !== id);
} else {
selected = [...selectedCheckbox, id];
}
setSelectedCheckbox(selected);
};
You click event handler takes the checkbox id as an argument and you should update you click handler accordingly:
<label key={checkbox.id}>
{checkbox.text}
<input
type="checkbox"
onChange={() => handleChange(checkbox.id)}
selected={selectedCheckbox.includes(checkbox.id)}
/>
</label>
I'm trying to make a search field with a dropdown list.For some reason, after mapping over an array and making a list of a few "li" the onClick handler sends to suggestionSelected (value) function 3 values without even clicking on any of them!
I'm enclosing the screenshot depicting my attempts to enter the name of a city starting with "s". See the output in the console.
The piece of code in question is commented.
import React from 'react';
import shortid from 'shortid';
import modules from './CityForm.module.css';
export default class ToDoForm extends React.Component {
state = {
text: '',
items: ["Moscow", "Saratov", "Singapore", "New York"],
suggestions: []
};
handleChange = (event) => {
this.setState({
text: event.target.value
});
const value = event.target.value;
let suggestions = [];
if (value.length > 0) {
const regex = new RegExp(`^${value}`, 'i');
suggestions = this.state.items.sort().filter(v => regex.test(v));
}
this.setState({
suggestions: suggestions
});
}
renderSuggestions() {
const { suggestions } = this.state;
if (suggestions.length === 0) {
return null;
} else {
return (
<ul className={modules.dropdown_list}>
{suggestions.map((item) => <li onClick={this.suggestionSelected(item)}> {item} </li>)}
</ul>
)
}
}
handleSubmit = (event) => {
event.preventDefault();
this.props.onSubmit({
id: shortid.generate(),
text: this.state.text
});
this.setState({
text: ''
});
}
suggestionSelected(value) {
console.log(value);
// this.setState({
// text: value,
// suggestions: []
// });
}
render() {
const { items } = this.state;
return (
<form
className={modules.search_row}
onSubmit={this.handleSubmit}>
<div>
<input
autoComplete="off"
className={modules.input}
name={modules.text}
value={this.state.text}
onChange={this.handleChange}
placeholder="E.g. Moscow..."
onKeyUp={this.filterOptions}
/>
{this.renderSuggestions()}
</div>
<button
className={modules.addcity_btn}
onClick={this.handleSubmit}>
Add city
</button>
</form>
)
}
}
try change your image function call to
{ suggestions.map((item) => <li onClick={() => this.suggestionSelected(item)}> {item} </li> ) }
by passing it as onClick={this.suggestionSelected(item)}, it is actually calling the function this.suggestionSelected(item) when rendering
if you wish to pass something back as a parameter in a callback assignment itself, pass it as a function reference
onClick={() => this.suggestionSelected(item)}
Wrong :<li onClick={this.suggestionSelected(item)}>
Correct: <li onclick={(evt)=>this.suggestionSelected(item) }
I've been stuck on this all day, and I'd appreciate some help. Thanks in advance.
At first I was writing it like this, but I'd get a type error: todos.map is not a function.
function toggleState() {
setTodos(state => ({ ...state, isComplete: !state.isComplete }))
}
Finally I realized that error was because it was returning todos as an object, so I tried this:
function toggleState() {
setKeywords(state => [{ ...state, isUsed: !state.isUsed }])
}
Now I'm not getting the type error, but it's still not working as expected. Here's the state before toggleState:
[
{
"name": "State",
"value": [
{
"todo": "Learn React",
"id": "91bad41d-1561-425a-9e77-960f731d058a",
"isComplete": false
}
]
and here's state after:
[
{
"name": "State",
"value": [
{
"0": {
"todo": "Learn React",
"id": "91bad41d-1561-425a-9e77-960f731d058a",
"isComplete": false
},
"isComplete": true
}
]
Here's the rest of my code:
import React, { useState, useEffect } from 'react'
import { uuid } from 'uuidv4'
import { Form, FormGroup, Input, Button } from 'reactstrap'
function Example(props) {
const [todos, setTodos] = useState([])
// Run when component first renders
useEffect(() => {
console.log('useEffect component first rendered')
if (localStorage.getItem('todoData')) {
setTodos(JSON.parse(localStorage.getItem('todoData')))
}
}, [])
// Run when todos state changes
useEffect(() => {
console.log('useEffect todos changed')
localStorage.setItem('todoData', JSON.stringify(todos))
}, [todos])
const [formInput, setFormInput] = useState()
function handleChange(e) {
setFormInput(e.target.value)
}
function handleSubmit(e) {
e.preventDefault()
setTodos(prev => prev.concat({ todo: formInput, id: uuid(), isComplete: false }))
setFormInput('')
}
function toggleState() {
setTodos(state => [{ ...state, isComplete: !state.isComplete }])
}
return (
<div className='text-center'>
<div className='mb-2 border text-center' style={{ height: '300px', overflowY: 'scroll' }}>
{todos.map(todo => (
<p className={todo.isUsed ? 'text-success my-1' : 'text-danger my-1'} key={todo.id}>
{todo.todo}
</p>
))}
</div>
<Form onSubmit={handleSubmit}>
<FormGroup>
<Input onChange={handleChange} type='text' name='text' id='todoForm' placeholder='Enter a todo' value={formInput || ''} />
<Button>Set Todo</Button>
</FormGroup>
</Form>
<Button onClick={toggleState}>Toggle isComplete</Button>
</div>
)
}
export default Example
The method that I end up using, and I've seen other developers do, is to copy the object or state first, do modifications to it, and then set the new state with the modified state.
I also noticed you need to provide an index for the todos to be able to toggle them, so I added that functionality.
Take a look at a working example, click "Run code snippet" below.
// main.js
// IGNORE THIS BECAUSE THIS IS JUST TO USE REACT IN STACK OVERFLOW
const { useEffect, useState } = React;
// ---- CODE STARTS HERE -----
const Example = (props) => {
const [todos, setTodos] = useState([]);
const [formInput, setFormInput] = useState('');
// Run when component first renders
useEffect(() => {
/*
// Uncomment - Just doesn't work in Stack Overflow
if (localStorage && localStorage.getItem('todoData')) {
setTodos(JSON.parse(localStorage.getItem('todoData')));
}
*/
}, []);
// Hooks
const handleChange = event => {
setFormInput(event.target.value);
};
const handleSubmit = event => {
const newTodosState = [...todos ]; // make copy
newTodosState.push({ todo: formInput, isComplete: false });
setTodos(newTodosState);
// Add functionality to update localStorage
// ex:
// localStorage.setItem('todoData', newTodosState);
// Reset form
setFormInput('');
event.preventDefault();
};
const toggleTodoState = index => event => {
const newTodosState = [...todos ]; // make copy
newTodosState[index].isComplete = !newTodosState[index].isComplete;
setTodos(newTodosState);
// Add functionality to update localStorage
};
const handleDelete = index => event => {
const newTodosState = [...todos.slice(0, index), ...todos.slice(index + 1) ];
setTodos(newTodosState);
// Add functionality to update localStorage
}
// Render
return (<div>
<h3>Todos</h3>
<ul>
{todos.map((item, index) => <li key={`todo-${index}`}>{item.todo} - <input type="checkbox" checked={item.isComplete} onClick={toggleTodoState(index)} /> - <button onClick={handleDelete(index)}>Delete</button></li>)}
</ul>
<hr />
<form onSubmit={handleSubmit}>
<input type="text" value={formInput} onChange={handleChange} placeholder="Enter todo name" />
<button type="submit">Add</button>
</form>
</div>);
};
ReactDOM.render(<Example />, document.querySelector('#root'));
<body>
<div id="root"></div>
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<script src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<script type="text/babel" src="main.js"></script>
</body>
So in your specific case, where you just want to toggle the isComplete on the first item, could be achieved like this:
function toggleState() {
setTodos(([firstItem, ...remainder]) => {
return [
{
...firstItem,
isComplete: !firstItem.isComplete
},
...remainder
];
});
}
Where we use Destructuring assignment to get the FirstItem and manipulate that, and spread the reminder back into the state.